2023-10-10 22:00:45 +00:00
package mysql
import (
"context"
2024-03-04 16:00:08 +00:00
"crypto/md5" //nolint:gosec
2023-10-10 22:00:45 +00:00
"database/sql"
2024-03-04 16:00:08 +00:00
"encoding/hex"
2024-02-13 18:03:53 +00:00
"errors"
2023-10-10 22:00:45 +00:00
"fmt"
2024-03-04 16:00:08 +00:00
"strings"
2023-10-10 22:00:45 +00:00
"time"
"unicode/utf8"
2024-02-02 20:57:46 +00:00
"github.com/fleetdm/fleet/v4/pkg/scripts"
2023-10-10 22:00:45 +00:00
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/google/uuid"
"github.com/jmoiron/sqlx"
)
func ( ds * Datastore ) NewHostScriptExecutionRequest ( ctx context . Context , request * fleet . HostScriptRequestPayload ) ( * fleet . HostScriptResult , error ) {
2024-02-13 18:03:53 +00:00
var res * fleet . HostScriptResult
return res , ds . withRetryTxx ( ctx , func ( tx sqlx . ExtContext ) error {
var err error
2024-03-04 16:00:08 +00:00
if request . ScriptContentID == 0 {
// then we are doing a sync execution, so create the contents first
2024-07-29 15:56:01 +00:00
scRes , err := insertScriptContents ( ctx , tx , request . ScriptContents )
2024-03-04 16:00:08 +00:00
if err != nil {
return err
}
id , _ := scRes . LastInsertId ( )
2024-10-18 17:38:26 +00:00
request . ScriptContentID = uint ( id ) //nolint:gosec // dismiss G115
2024-03-04 16:00:08 +00:00
}
2024-07-29 15:56:01 +00:00
res , err = newHostScriptExecutionRequest ( ctx , tx , request )
2024-02-13 18:03:53 +00:00
return err
} )
}
2024-07-29 15:56:01 +00:00
func newHostScriptExecutionRequest ( ctx context . Context , tx sqlx . ExtContext , request * fleet . HostScriptRequestPayload ) ( * fleet . HostScriptResult , error ) {
2023-10-10 22:00:45 +00:00
const (
2024-10-08 20:45:31 +00:00
insStmt = ` INSERT INTO host_script_results (host_id, execution_id, script_content_id, output, script_id, policy_id, user_id, sync_request) VALUES (?, ?, ?, '', ?, ?, ?, ?) `
getStmt = ` SELECT hsr.id, hsr.host_id, hsr.execution_id, hsr.created_at, hsr.script_id, hsr.policy_id, hsr.user_id, hsr.sync_request, sc.contents as script_contents FROM host_script_results hsr JOIN script_contents sc WHERE sc.id = hsr.script_content_id AND hsr.id = ? `
2023-10-10 22:00:45 +00:00
)
execID := uuid . New ( ) . String ( )
2024-02-13 18:03:53 +00:00
result , err := tx . ExecContext ( ctx , insStmt ,
2023-10-10 22:00:45 +00:00
request . HostID ,
execID ,
2024-03-04 16:00:08 +00:00
request . ScriptContentID ,
2023-10-10 22:00:45 +00:00
request . ScriptID ,
2024-10-08 20:45:31 +00:00
request . PolicyID ,
2024-01-29 14:37:54 +00:00
request . UserID ,
request . SyncRequest ,
2023-10-10 22:00:45 +00:00
)
if err != nil {
return nil , ctxerr . Wrap ( ctx , err , "new host script execution request" )
}
var script fleet . HostScriptResult
id , _ := result . LastInsertId ( )
2024-02-13 18:03:53 +00:00
err = sqlx . GetContext ( ctx , tx , & script , getStmt , id )
if err != nil {
2023-10-10 22:00:45 +00:00
return nil , ctxerr . Wrap ( ctx , err , "getting the created host script result to return" )
}
2024-02-13 18:03:53 +00:00
2023-10-10 22:00:45 +00:00
return & script , nil
}
2024-05-15 22:39:42 +00:00
func truncateScriptResult ( output string ) string {
const maxOutputRuneLen = 10000
if len ( output ) > utf8 . UTFMax * maxOutputRuneLen {
// truncate the bytes as we know the output is too long, no point
// converting more bytes than needed to runes.
output = output [ len ( output ) - ( utf8 . UTFMax * maxOutputRuneLen ) : ]
}
if utf8 . RuneCountInString ( output ) > maxOutputRuneLen {
outputRunes := [ ] rune ( output )
output = string ( outputRunes [ len ( outputRunes ) - maxOutputRuneLen : ] )
}
return output
}
2024-09-05 19:20:36 +00:00
func ( ds * Datastore ) SetHostScriptExecutionResult ( ctx context . Context , result * fleet . HostScriptResultPayload ) ( * fleet . HostScriptResult ,
2024-09-13 12:54:10 +00:00
string , error ,
) {
2024-02-15 17:05:13 +00:00
const resultExistsStmt = `
SELECT
1
FROM
host_script_results
WHERE
host_id = ? AND
execution_id = ? AND
exit_code IS NOT NULL
`
2023-10-10 22:00:45 +00:00
const updStmt = `
UPDATE host_script_results SET
output = ? ,
runtime = ? ,
2024-07-10 20:33:39 +00:00
exit_code = ? ,
timeout = ?
2023-10-10 22:00:45 +00:00
WHERE
host_id = ? AND
execution_id = ? `
2024-02-13 18:03:53 +00:00
const hostMDMActionsStmt = `
SELECT
CASE
2024-09-05 19:20:36 +00:00
WHEN lock_ref = : execution_id THEN ' lock_ref '
WHEN unlock_ref = : execution_id THEN ' unlock_ref '
WHEN wipe_ref = : execution_id THEN ' wipe_ref '
2024-02-13 18:03:53 +00:00
ELSE ' '
2024-09-05 19:20:36 +00:00
END AS action
2024-02-13 18:03:53 +00:00
FROM
host_mdm_actions
WHERE
2024-09-05 19:20:36 +00:00
host_id = : host_id
UNION
SELECT ' uninstall ' AS action
FROM
host_software_installs
WHERE
execution_id = : execution_id AND host_id = : host_id
2024-02-13 18:03:53 +00:00
`
2024-05-15 22:39:42 +00:00
output := truncateScriptResult ( result . Output )
2023-10-10 22:00:45 +00:00
2024-01-29 14:37:54 +00:00
var hsr * fleet . HostScriptResult
2024-09-05 19:20:36 +00:00
var action string
2024-02-13 18:03:53 +00:00
err := ds . withRetryTxx ( ctx , func ( tx sqlx . ExtContext ) error {
2024-02-15 17:05:13 +00:00
var resultExists bool
err := sqlx . GetContext ( ctx , tx , & resultExists , resultExistsStmt , result . HostID , result . ExecutionID )
if err != nil && ! errors . Is ( err , sql . ErrNoRows ) {
return ctxerr . Wrap ( ctx , err , "check if host script result exists" )
}
if resultExists {
// succeed but leave hsr nil
return nil
}
2024-02-13 18:03:53 +00:00
res , err := tx . ExecContext ( ctx , updStmt ,
output ,
result . Runtime ,
2024-04-16 14:53:50 +00:00
// Windows error codes are signed 32-bit integers, but are
// returned as unsigned integers by the windows API. The
// software that receives them is responsible for casting
// it to a 32-bit signed integer.
// See /orbit/pkg/scripts/exec_windows.go
2024-10-18 17:38:26 +00:00
int32 ( result . ExitCode ) , //nolint:gosec // dismiss G115
2024-07-10 20:33:39 +00:00
result . Timeout ,
2024-02-13 18:03:53 +00:00
result . HostID ,
result . ExecutionID ,
)
2024-01-29 14:37:54 +00:00
if err != nil {
2024-02-13 18:03:53 +00:00
return ctxerr . Wrap ( ctx , err , "update host script result" )
}
if n , _ := res . RowsAffected ( ) ; n > 0 {
// it did update, so return the updated result
hsr , err = ds . getHostScriptExecutionResultDB ( ctx , tx , result . ExecutionID )
if err != nil {
return ctxerr . Wrap ( ctx , err , "load updated host script result" )
}
2024-09-05 19:20:36 +00:00
// look up if that script was a lock/unlock/wipe/uninstall script for that host,
2024-02-13 18:03:53 +00:00
// and if so update the host_mdm_actions table accordingly.
2024-09-05 19:20:36 +00:00
namedArgs := map [ string ] any {
"host_id" : result . HostID ,
"execution_id" : result . ExecutionID ,
}
stmt , args , err := sqlx . Named ( hostMDMActionsStmt , namedArgs )
if err != nil {
return ctxerr . Wrap ( ctx , err , "build named query for host mdm actions" )
}
err = sqlx . GetContext ( ctx , tx , & action , stmt , args ... )
2024-02-13 18:03:53 +00:00
if err != nil && ! errors . Is ( err , sql . ErrNoRows ) { // ignore ErrNoRows, refCol will be empty
return ctxerr . Wrap ( ctx , err , "lookup host script corresponding mdm action" )
}
2024-09-05 19:20:36 +00:00
switch action {
case "" :
// do nothing
case "uninstall" :
err = updateUninstallStatusFromResult ( ctx , tx , result . HostID , result . ExecutionID , result . ExitCode )
if err != nil {
return ctxerr . Wrap ( ctx , err , "update host uninstall action based on script result" )
}
default : // lock/unlock/wipe
err = updateHostLockWipeStatusFromResult ( ctx , tx , result . HostID , action , result . ExitCode == 0 )
2024-02-13 18:03:53 +00:00
if err != nil {
return ctxerr . Wrap ( ctx , err , "update host mdm action based on script result" )
}
}
2024-01-29 14:37:54 +00:00
}
2024-02-13 18:03:53 +00:00
return nil
} )
if err != nil {
2024-09-05 19:20:36 +00:00
return nil , "" , err
2024-01-29 14:37:54 +00:00
}
2024-09-05 19:20:36 +00:00
return hsr , action , nil
2023-10-10 22:00:45 +00:00
}
2024-01-29 14:37:54 +00:00
func ( ds * Datastore ) ListPendingHostScriptExecutions ( ctx context . Context , hostID uint ) ( [ ] * fleet . HostScriptResult , error ) {
2023-10-10 22:00:45 +00:00
const listStmt = `
SELECT
id ,
host_id ,
execution_id ,
2024-03-13 19:53:08 +00:00
script_id
2023-10-10 22:00:45 +00:00
FROM
host_script_results
WHERE
host_id = ? AND
2024-01-29 14:37:54 +00:00
exit_code IS NULL
2024-02-02 20:57:46 +00:00
-- async requests + sync requests created within the given interval
AND (
sync_request = 0
OR created_at >= DATE_SUB ( NOW ( ) , INTERVAL ? SECOND )
)
2024-01-29 14:37:54 +00:00
ORDER BY
created_at ASC `
2023-10-10 22:00:45 +00:00
var results [ ] * fleet . HostScriptResult
2024-02-02 20:57:46 +00:00
seconds := int ( scripts . MaxServerWaitTime . Seconds ( ) )
if err := sqlx . SelectContext ( ctx , ds . reader ( ctx ) , & results , listStmt , hostID , seconds ) ; err != nil {
2024-01-29 14:37:54 +00:00
return nil , ctxerr . Wrap ( ctx , err , "list pending host script executions" )
}
return results , nil
}
2024-10-04 01:03:40 +00:00
func ( ds * Datastore ) IsExecutionPendingForHost ( ctx context . Context , hostID uint , scriptID uint ) ( bool , error ) {
2024-01-29 14:37:54 +00:00
const getStmt = `
2024-02-13 18:03:53 +00:00
SELECT
1
2024-01-29 14:37:54 +00:00
FROM
host_script_results
WHERE
host_id = ? AND
script_id = ? AND
exit_code IS NULL
`
var results [ ] * uint
if err := sqlx . SelectContext ( ctx , ds . reader ( ctx ) , & results , getStmt , hostID , scriptID ) ; err != nil {
2024-10-04 01:03:40 +00:00
return false , ctxerr . Wrap ( ctx , err , "is execution pending for host" )
2023-10-10 22:00:45 +00:00
}
2024-10-04 01:03:40 +00:00
return len ( results ) > 0 , nil
2023-10-10 22:00:45 +00:00
}
func ( ds * Datastore ) GetHostScriptExecutionResult ( ctx context . Context , execID string ) ( * fleet . HostScriptResult , error ) {
2024-01-29 14:37:54 +00:00
return ds . getHostScriptExecutionResultDB ( ctx , ds . reader ( ctx ) , execID )
}
func ( ds * Datastore ) getHostScriptExecutionResultDB ( ctx context . Context , q sqlx . QueryerContext , execID string ) ( * fleet . HostScriptResult , error ) {
2023-10-10 22:00:45 +00:00
const getStmt = `
SELECT
2024-03-04 16:00:08 +00:00
hsr . id ,
hsr . host_id ,
hsr . execution_id ,
sc . contents as script_contents ,
hsr . script_id ,
2024-10-08 20:45:31 +00:00
hsr . policy_id ,
2024-03-04 16:00:08 +00:00
hsr . output ,
hsr . runtime ,
hsr . exit_code ,
2024-07-10 20:33:39 +00:00
hsr . timeout ,
2024-03-04 16:00:08 +00:00
hsr . created_at ,
hsr . user_id ,
2024-06-12 14:26:03 +00:00
hsr . sync_request ,
hsr . host_deleted_at
2023-10-10 22:00:45 +00:00
FROM
2024-03-04 16:00:08 +00:00
host_script_results hsr
JOIN
script_contents sc
2023-10-10 22:00:45 +00:00
WHERE
2024-03-04 16:00:08 +00:00
hsr . execution_id = ?
AND
hsr . script_content_id = sc . id
`
2023-10-10 22:00:45 +00:00
var result fleet . HostScriptResult
2024-01-29 14:37:54 +00:00
if err := sqlx . GetContext ( ctx , q , & result , getStmt , execID ) ; err != nil {
2023-10-10 22:00:45 +00:00
if err == sql . ErrNoRows {
return nil , ctxerr . Wrap ( ctx , notFound ( "HostScriptResult" ) . WithName ( execID ) )
}
return nil , ctxerr . Wrap ( ctx , err , "get host script result" )
}
return & result , nil
}
func ( ds * Datastore ) NewScript ( ctx context . Context , script * fleet . Script ) ( * fleet . Script , error ) {
2024-03-04 16:00:08 +00:00
var res sql . Result
err := ds . withRetryTxx ( ctx , func ( tx sqlx . ExtContext ) error {
var err error
// first insert script contents
2024-07-29 15:56:01 +00:00
scRes , err := insertScriptContents ( ctx , tx , script . ScriptContents )
2024-03-04 16:00:08 +00:00
if err != nil {
return err
}
id , _ := scRes . LastInsertId ( )
// then create the script entity
2024-10-18 17:38:26 +00:00
res , err = insertScript ( ctx , tx , script , uint ( id ) ) //nolint:gosec // dismiss G115
2024-03-04 16:00:08 +00:00
return err
} )
if err != nil {
return nil , err
}
id , _ := res . LastInsertId ( )
2024-10-18 17:38:26 +00:00
return ds . getScriptDB ( ctx , ds . writer ( ctx ) , uint ( id ) ) //nolint:gosec // dismiss G115
2024-03-04 16:00:08 +00:00
}
2024-07-29 15:56:01 +00:00
func insertScript ( ctx context . Context , tx sqlx . ExtContext , script * fleet . Script , scriptContentsID uint ) ( sql . Result , error ) {
2023-10-10 22:00:45 +00:00
const insertStmt = `
INSERT INTO
scripts (
2024-03-13 19:53:08 +00:00
team_id , global_or_team_id , name , script_content_id
2023-10-10 22:00:45 +00:00
)
VALUES
2024-03-13 19:53:08 +00:00
( ? , ? , ? , ? )
2023-10-10 22:00:45 +00:00
`
var globalOrTeamID uint
if script . TeamID != nil {
globalOrTeamID = * script . TeamID
}
2024-03-04 16:00:08 +00:00
res , err := tx . ExecContext ( ctx , insertStmt ,
2024-03-13 19:53:08 +00:00
script . TeamID , globalOrTeamID , script . Name , scriptContentsID )
2023-10-10 22:00:45 +00:00
if err != nil {
2024-05-30 21:18:42 +00:00
if IsDuplicate ( err ) {
2023-10-10 22:00:45 +00:00
// name already exists for this team/global
err = alreadyExists ( "Script" , script . Name )
} else if isChildForeignKeyError ( err ) {
// team does not exist
err = foreignKey ( "scripts" , fmt . Sprintf ( "team_id=%v" , script . TeamID ) )
}
return nil , ctxerr . Wrap ( ctx , err , "insert script" )
}
2024-03-04 16:00:08 +00:00
return res , nil
}
2024-07-29 15:56:01 +00:00
func insertScriptContents ( ctx context . Context , tx sqlx . ExtContext , contents string ) ( sql . Result , error ) {
2024-03-04 16:00:08 +00:00
const insertStmt = `
INSERT INTO
script_contents (
md5_checksum , contents
)
VALUES ( UNHEX ( ? ) , ? )
ON DUPLICATE KEY UPDATE
id = LAST_INSERT_ID ( id )
`
md5Checksum := md5ChecksumScriptContent ( contents )
res , err := tx . ExecContext ( ctx , insertStmt , md5Checksum , contents )
if err != nil {
return nil , ctxerr . Wrap ( ctx , err , "insert script contents" )
}
return res , nil
}
func md5ChecksumScriptContent ( s string ) string {
2024-05-30 21:18:42 +00:00
return md5ChecksumBytes ( [ ] byte ( s ) )
}
func md5ChecksumBytes ( b [ ] byte ) string {
rawChecksum := md5 . Sum ( b ) //nolint:gosec
2024-03-04 16:00:08 +00:00
return strings . ToUpper ( hex . EncodeToString ( rawChecksum [ : ] ) )
2023-10-10 22:00:45 +00:00
}
func ( ds * Datastore ) Script ( ctx context . Context , id uint ) ( * fleet . Script , error ) {
return ds . getScriptDB ( ctx , ds . reader ( ctx ) , id )
}
func ( ds * Datastore ) getScriptDB ( ctx context . Context , q sqlx . QueryerContext , id uint ) ( * fleet . Script , error ) {
const getStmt = `
SELECT
id ,
team_id ,
name ,
created_at ,
2024-03-04 16:00:08 +00:00
updated_at ,
script_content_id
2023-10-10 22:00:45 +00:00
FROM
scripts
WHERE
id = ?
`
var script fleet . Script
if err := sqlx . GetContext ( ctx , q , & script , getStmt , id ) ; err != nil {
if err == sql . ErrNoRows {
return nil , notFound ( "Script" ) . WithID ( id )
}
return nil , ctxerr . Wrap ( ctx , err , "get script" )
}
return & script , nil
}
func ( ds * Datastore ) GetScriptContents ( ctx context . Context , id uint ) ( [ ] byte , error ) {
const getStmt = `
SELECT
2024-03-04 16:00:08 +00:00
sc . contents
2023-10-10 22:00:45 +00:00
FROM
2024-03-04 16:00:08 +00:00
script_contents sc
JOIN scripts s
2023-10-10 22:00:45 +00:00
WHERE
2024-03-04 16:00:08 +00:00
s . script_content_id = sc . id
AND s . id = ? ;
2023-10-10 22:00:45 +00:00
`
var contents [ ] byte
if err := sqlx . GetContext ( ctx , ds . reader ( ctx ) , & contents , getStmt , id ) ; err != nil {
if err == sql . ErrNoRows {
return nil , notFound ( "Script" ) . WithID ( id )
}
return nil , ctxerr . Wrap ( ctx , err , "get script contents" )
}
return contents , nil
}
2024-09-05 19:20:36 +00:00
func ( ds * Datastore ) GetAnyScriptContents ( ctx context . Context , id uint ) ( [ ] byte , error ) {
const getStmt = `
SELECT
sc . contents
FROM
script_contents sc
WHERE
sc . id = ?
`
var contents [ ] byte
if err := sqlx . GetContext ( ctx , ds . reader ( ctx ) , & contents , getStmt , id ) ; err != nil {
if errors . Is ( err , sql . ErrNoRows ) {
return nil , notFound ( "Script" ) . WithID ( id )
}
return nil , ctxerr . Wrap ( ctx , err , "get any script contents" )
}
return contents , nil
}
2024-10-04 01:03:40 +00:00
var errDeleteScriptWithAssociatedPolicy = & fleet . ConflictError { Message : "Couldn't delete. Policy automation uses this script. Please remove this script from associated policy automations and try again." }
2023-10-10 22:00:45 +00:00
func ( ds * Datastore ) DeleteScript ( ctx context . Context , id uint ) error {
_ , err := ds . writer ( ctx ) . ExecContext ( ctx , ` DELETE FROM scripts WHERE id = ? ` , id )
if err != nil {
2024-10-04 01:03:40 +00:00
if isMySQLForeignKey ( err ) {
// Check if the script is referenced by a policy automation.
var count int
if err := sqlx . GetContext ( ctx , ds . reader ( ctx ) , & count , ` SELECT COUNT(*) FROM policies WHERE script_id = ? ` , id ) ; err != nil {
return ctxerr . Wrapf ( ctx , err , "getting reference from policies" )
}
if count > 0 {
return ctxerr . Wrap ( ctx , errDeleteScriptWithAssociatedPolicy , "delete script" )
}
}
2023-10-10 22:00:45 +00:00
return ctxerr . Wrap ( ctx , err , "delete script" )
}
return nil
}
func ( ds * Datastore ) ListScripts ( ctx context . Context , teamID * uint , opt fleet . ListOptions ) ( [ ] * fleet . Script , * fleet . PaginationMetadata , error ) {
var scripts [ ] * fleet . Script
const selectStmt = `
SELECT
s . id ,
s . team_id ,
s . name ,
s . created_at ,
s . updated_at
FROM
scripts s
WHERE
s . global_or_team_id = ?
`
var globalOrTeamID uint
if teamID != nil {
globalOrTeamID = * teamID
}
args := [ ] any { globalOrTeamID }
stmt , args := appendListOptionsWithCursorToSQL ( selectStmt , args , & opt )
if err := sqlx . SelectContext ( ctx , ds . reader ( ctx ) , & scripts , stmt , args ... ) ; err != nil {
return nil , nil , ctxerr . Wrap ( ctx , err , "select scripts" )
}
var metaData * fleet . PaginationMetadata
if opt . IncludeMetadata {
metaData = & fleet . PaginationMetadata { HasPreviousResults : opt . Page > 0 }
2024-10-18 17:38:26 +00:00
if len ( scripts ) > int ( opt . PerPage ) { //nolint:gosec // dismiss G115
2023-10-10 22:00:45 +00:00
metaData . HasNextResults = true
scripts = scripts [ : len ( scripts ) - 1 ]
}
}
return scripts , metaData , nil
}
2024-03-05 14:53:17 +00:00
func ( ds * Datastore ) GetScriptIDByName ( ctx context . Context , name string , teamID * uint ) ( uint , error ) {
const selectStmt = `
SELECT
id
FROM
scripts
WHERE
global_or_team_id = ?
AND name = ?
`
var globalOrTeamID uint
if teamID != nil {
globalOrTeamID = * teamID
}
var id uint
if err := sqlx . GetContext ( ctx , ds . reader ( ctx ) , & id , selectStmt , globalOrTeamID , name ) ; err != nil {
if err == sql . ErrNoRows {
return 0 , notFound ( "Script" ) . WithName ( name )
}
return 0 , ctxerr . Wrap ( ctx , err , "get script by name" )
}
return id , nil
}
2023-11-14 14:23:51 +00:00
func ( ds * Datastore ) GetHostScriptDetails ( ctx context . Context , hostID uint , teamID * uint , opt fleet . ListOptions , hostPlatform string ) ( [ ] * fleet . HostScriptDetail , * fleet . PaginationMetadata , error ) {
2023-10-10 22:00:45 +00:00
var globalOrTeamID uint
if teamID != nil {
globalOrTeamID = * teamID
}
2023-11-14 14:23:51 +00:00
var extension string
2024-02-07 21:32:51 +00:00
switch {
case hostPlatform == "windows" :
// filter by .ps1 extension
2023-11-14 14:23:51 +00:00
extension = ` %.ps1 `
2024-02-07 21:32:51 +00:00
case fleet . IsUnixLike ( hostPlatform ) :
// filter by .sh extension
extension = ` %.sh `
default :
// no extension filter
2023-11-14 14:23:51 +00:00
}
2023-10-10 22:00:45 +00:00
type row struct {
ScriptID uint ` db:"script_id" `
Name string ` db:"name" `
HSRID * uint ` db:"hsr_id" `
ExecutionID * string ` db:"execution_id" `
ExecutedAt * time . Time ` db:"executed_at" `
ExitCode * int64 ` db:"exit_code" `
}
sql := `
SELECT
s . id AS script_id ,
s . name ,
hsr . id AS hsr_id ,
hsr . created_at AS executed_at ,
hsr . execution_id ,
hsr . exit_code
FROM
scripts s
LEFT JOIN (
SELECT
id ,
2024-01-29 14:37:54 +00:00
host_id ,
2023-10-10 22:00:45 +00:00
script_id ,
execution_id ,
created_at ,
exit_code
FROM
host_script_results r
WHERE
host_id = ?
AND NOT EXISTS (
SELECT
1
FROM
host_script_results
WHERE
host_id = ?
AND id != r . id
AND script_id = r . script_id
AND ( created_at > r . created_at
OR ( created_at = r . created_at
AND id > r . id ) ) ) ) hsr
ON s . id = hsr . script_id
WHERE
( hsr . host_id IS NULL OR hsr . host_id = ? )
2023-11-14 14:23:51 +00:00
AND s . global_or_team_id = ?
`
2023-10-10 22:00:45 +00:00
args := [ ] any { hostID , hostID , hostID , globalOrTeamID }
2023-11-14 14:23:51 +00:00
if len ( extension ) > 0 {
args = append ( args , extension )
sql += `
AND s . name LIKE ?
`
}
2023-10-10 22:00:45 +00:00
stmt , args := appendListOptionsWithCursorToSQL ( sql , args , & opt )
var rows [ ] * row
if err := sqlx . SelectContext ( ctx , ds . reader ( ctx ) , & rows , stmt , args ... ) ; err != nil {
return nil , nil , ctxerr . Wrap ( ctx , err , "get host script details" )
}
var metaData * fleet . PaginationMetadata
if opt . IncludeMetadata {
metaData = & fleet . PaginationMetadata { HasPreviousResults : opt . Page > 0 }
2024-10-18 17:38:26 +00:00
if len ( rows ) > int ( opt . PerPage ) { //nolint:gosec // dismiss G115
2023-10-10 22:00:45 +00:00
metaData . HasNextResults = true
rows = rows [ : len ( rows ) - 1 ]
}
}
results := make ( [ ] * fleet . HostScriptDetail , 0 , len ( rows ) )
for _ , r := range rows {
results = append ( results , fleet . NewHostScriptDetail ( hostID , r . ScriptID , r . Name , r . ExecutionID , r . ExecutedAt , r . ExitCode , r . HSRID ) )
}
return results , metaData , nil
}
2024-10-04 01:03:40 +00:00
func ( ds * Datastore ) BatchSetScripts ( ctx context . Context , tmID * uint , scripts [ ] * fleet . Script ) ( [ ] fleet . ScriptResponse , error ) {
2023-10-10 22:00:45 +00:00
const loadExistingScripts = `
SELECT
name
FROM
scripts
WHERE
global_or_team_id = ? AND
name IN ( ? )
`
const deleteAllScriptsInTeam = `
DELETE FROM
scripts
WHERE
global_or_team_id = ?
2024-10-04 01:03:40 +00:00
`
const unsetAllScriptsFromPolicies = ` UPDATE policies SET script_id = NULL WHERE team_id = ? `
const unsetScriptsNotInListFromPolicies = `
UPDATE policies SET script_id = NULL
WHERE script_id IN ( SELECT id FROM scripts WHERE global_or_team_id = ? AND name NOT IN ( ? ) )
2023-10-10 22:00:45 +00:00
`
const deleteScriptsNotInList = `
DELETE FROM
scripts
WHERE
global_or_team_id = ? AND
name NOT IN ( ? )
`
const insertNewOrEditedScript = `
INSERT INTO
scripts (
2024-03-13 19:53:08 +00:00
team_id , global_or_team_id , name , script_content_id
2023-10-10 22:00:45 +00:00
)
VALUES
2024-03-13 19:53:08 +00:00
( ? , ? , ? , ? )
2023-10-10 22:00:45 +00:00
ON DUPLICATE KEY UPDATE
2024-03-04 16:00:08 +00:00
script_content_id = VALUES ( script_content_id )
2023-10-10 22:00:45 +00:00
`
2024-10-04 01:03:40 +00:00
const loadInsertedScripts = ` SELECT id, team_id, name FROM scripts WHERE global_or_team_id = ? `
2023-10-10 22:00:45 +00:00
// use a team id of 0 if no-team
var globalOrTeamID uint
if tmID != nil {
globalOrTeamID = * tmID
}
// build a list of names for the incoming scripts, will keep the
// existing ones if there's a match and no change
incomingNames := make ( [ ] string , len ( scripts ) )
// at the same time, index the incoming scripts keyed by name for ease
// of processing
incomingScripts := make ( map [ string ] * fleet . Script , len ( scripts ) )
for i , p := range scripts {
incomingNames [ i ] = p . Name
incomingScripts [ p . Name ] = p
}
2024-10-04 01:03:40 +00:00
var insertedScripts [ ] fleet . ScriptResponse
if err := ds . withRetryTxx ( ctx , func ( tx sqlx . ExtContext ) error {
2023-10-10 22:00:45 +00:00
var existingScripts [ ] * fleet . Script
if len ( incomingNames ) > 0 {
// load existing scripts that match the incoming scripts by names
stmt , args , err := sqlx . In ( loadExistingScripts , globalOrTeamID , incomingNames )
if err != nil {
return ctxerr . Wrap ( ctx , err , "build query to load existing scripts" )
}
if err := sqlx . SelectContext ( ctx , tx , & existingScripts , stmt , args ... ) ; err != nil {
return ctxerr . Wrap ( ctx , err , "load existing scripts" )
}
}
// figure out if we need to delete any scripts
keepNames := make ( [ ] string , 0 , len ( incomingNames ) )
for _ , p := range existingScripts {
if newS := incomingScripts [ p . Name ] ; newS != nil {
keepNames = append ( keepNames , p . Name )
}
}
var (
2024-10-04 01:03:40 +00:00
scriptsStmt string
scriptsArgs [ ] any
policiesStmt string
policiesArgs [ ] any
err error
2023-10-10 22:00:45 +00:00
)
if len ( keepNames ) > 0 {
// delete the obsolete scripts
2024-10-04 01:03:40 +00:00
scriptsStmt , scriptsArgs , err = sqlx . In ( deleteScriptsNotInList , globalOrTeamID , keepNames )
2023-10-10 22:00:45 +00:00
if err != nil {
return ctxerr . Wrap ( ctx , err , "build statement to delete obsolete scripts" )
}
2024-10-04 01:03:40 +00:00
policiesStmt , policiesArgs , err = sqlx . In ( unsetScriptsNotInListFromPolicies , globalOrTeamID , keepNames )
if err != nil {
return ctxerr . Wrap ( ctx , err , "build statement to unset obsolete scripts from policies" )
}
2023-10-10 22:00:45 +00:00
} else {
2024-10-04 01:03:40 +00:00
scriptsStmt = deleteAllScriptsInTeam
scriptsArgs = [ ] any { globalOrTeamID }
policiesStmt = unsetAllScriptsFromPolicies
policiesArgs = [ ] any { globalOrTeamID }
}
if _ , err := tx . ExecContext ( ctx , policiesStmt , policiesArgs ... ) ; err != nil {
return ctxerr . Wrap ( ctx , err , "unset obsolete scripts from policies" )
2023-10-10 22:00:45 +00:00
}
2024-10-04 01:03:40 +00:00
if _ , err := tx . ExecContext ( ctx , scriptsStmt , scriptsArgs ... ) ; err != nil {
2023-10-10 22:00:45 +00:00
return ctxerr . Wrap ( ctx , err , "delete obsolete scripts" )
}
// insert the new scripts and the ones that have changed
for _ , s := range incomingScripts {
2024-07-29 15:56:01 +00:00
scRes , err := insertScriptContents ( ctx , tx , s . ScriptContents )
2024-03-04 16:00:08 +00:00
if err != nil {
return ctxerr . Wrapf ( ctx , err , "inserting script contents for script with name %q" , s . Name )
}
id , _ := scRes . LastInsertId ( )
2024-10-18 17:38:26 +00:00
if _ , err := tx . ExecContext ( ctx , insertNewOrEditedScript , tmID , globalOrTeamID , s . Name ,
uint ( id ) ) ; err != nil { //nolint:gosec // dismiss G115
2023-10-10 22:00:45 +00:00
return ctxerr . Wrapf ( ctx , err , "insert new/edited script with name %q" , s . Name )
}
}
2024-10-04 01:03:40 +00:00
if err := sqlx . SelectContext ( ctx , tx , & insertedScripts , loadInsertedScripts , globalOrTeamID ) ; err != nil {
return ctxerr . Wrap ( ctx , err , "load inserted scripts" )
}
2023-10-10 22:00:45 +00:00
return nil
2024-10-04 01:03:40 +00:00
} ) ; err != nil {
return nil , err
}
return insertedScripts , nil
2023-10-10 22:00:45 +00:00
}
2024-02-13 18:03:53 +00:00
2024-02-26 16:31:00 +00:00
func ( ds * Datastore ) GetHostLockWipeStatus ( ctx context . Context , host * fleet . Host ) ( * fleet . HostLockWipeStatus , error ) {
2024-02-13 18:03:53 +00:00
const stmt = `
SELECT
lock_ref ,
wipe_ref ,
unlock_ref ,
2024-02-26 21:52:23 +00:00
unlock_pin ,
fleet_platform
2024-02-13 18:03:53 +00:00
FROM
host_mdm_actions
WHERE
host_id = ?
`
var mdmActions struct {
2024-02-26 21:52:23 +00:00
LockRef * string ` db:"lock_ref" `
WipeRef * string ` db:"wipe_ref" `
UnlockRef * string ` db:"unlock_ref" `
UnlockPIN * string ` db:"unlock_pin" `
FleetPlatform string ` db:"fleet_platform" `
2024-02-13 18:03:53 +00:00
}
2024-02-26 16:31:00 +00:00
fleetPlatform := host . FleetPlatform ( )
2024-02-13 18:03:53 +00:00
status := & fleet . HostLockWipeStatus {
HostFleetPlatform : fleetPlatform ,
}
2024-02-26 16:31:00 +00:00
if err := sqlx . GetContext ( ctx , ds . reader ( ctx ) , & mdmActions , stmt , host . ID ) ; err != nil {
2024-02-13 18:03:53 +00:00
if err == sql . ErrNoRows {
// do not return a Not Found error, return the zero-value status, which
// will report the correct states.
return status , nil
}
return nil , ctxerr . Wrap ( ctx , err , "get host lock/wipe status" )
}
2024-02-26 21:52:23 +00:00
// if we have a fleet platform stored in host_mdm_actions, use it instead of
// the host.FleetPlatform() because the platform can be overwritten with an
// unknown OS name when a Wipe gets executed.
if mdmActions . FleetPlatform != "" {
fleetPlatform = mdmActions . FleetPlatform
status . HostFleetPlatform = fleetPlatform
}
2024-02-13 18:03:53 +00:00
switch fleetPlatform {
2024-05-28 22:17:14 +00:00
case "darwin" , "ios" , "ipados" :
2024-02-13 18:03:53 +00:00
if mdmActions . UnlockPIN != nil {
status . UnlockPIN = * mdmActions . UnlockPIN
}
if mdmActions . UnlockRef != nil {
var err error
status . UnlockRequestedAt , err = time . Parse ( time . DateTime , * mdmActions . UnlockRef )
if err != nil {
// if the format is unexpected but there's something in UnlockRef, just
// replace it with the current timestamp, it should still indicate that
// an unlock was requested (e.g. in case someone plays with the data
// directly in the DB and messes up the format).
status . UnlockRequestedAt = time . Now ( ) . UTC ( )
}
}
if mdmActions . LockRef != nil {
// the lock reference is an MDM command
2024-02-26 16:31:00 +00:00
cmd , cmdRes , err := ds . getHostMDMAppleCommand ( ctx , * mdmActions . LockRef , host . UUID )
2024-02-13 18:03:53 +00:00
if err != nil {
2024-02-26 16:31:00 +00:00
return nil , ctxerr . Wrap ( ctx , err , "get lock reference" )
2024-02-13 18:03:53 +00:00
}
status . LockMDMCommand = cmd
2024-02-26 16:31:00 +00:00
status . LockMDMCommandResult = cmdRes
}
2024-02-13 18:03:53 +00:00
2024-02-26 16:31:00 +00:00
if mdmActions . WipeRef != nil {
// the wipe reference is an MDM command
cmd , cmdRes , err := ds . getHostMDMAppleCommand ( ctx , * mdmActions . WipeRef , host . UUID )
2024-02-13 18:03:53 +00:00
if err != nil {
2024-02-26 16:31:00 +00:00
return nil , ctxerr . Wrap ( ctx , err , "get wipe reference" )
2024-02-13 18:03:53 +00:00
}
2024-02-26 16:31:00 +00:00
status . WipeMDMCommand = cmd
status . WipeMDMCommandResult = cmdRes
2024-02-13 18:03:53 +00:00
}
case "windows" , "linux" :
// lock and unlock references are scripts
if mdmActions . LockRef != nil {
hsr , err := ds . getHostScriptExecutionResultDB ( ctx , ds . reader ( ctx ) , * mdmActions . LockRef )
if err != nil {
return nil , ctxerr . Wrap ( ctx , err , "get lock reference script result" )
}
status . LockScript = hsr
}
if mdmActions . UnlockRef != nil {
hsr , err := ds . getHostScriptExecutionResultDB ( ctx , ds . reader ( ctx ) , * mdmActions . UnlockRef )
if err != nil {
return nil , ctxerr . Wrap ( ctx , err , "get unlock reference script result" )
}
status . UnlockScript = hsr
}
2024-02-26 16:31:00 +00:00
// wipe is an MDM command on Windows, a script on Linux
if mdmActions . WipeRef != nil {
if fleetPlatform == "windows" {
cmd , cmdRes , err := ds . getHostMDMWindowsCommand ( ctx , * mdmActions . WipeRef , host . UUID )
if err != nil {
return nil , ctxerr . Wrap ( ctx , err , "get wipe reference" )
}
status . WipeMDMCommand = cmd
status . WipeMDMCommandResult = cmdRes
} else {
hsr , err := ds . getHostScriptExecutionResultDB ( ctx , ds . reader ( ctx ) , * mdmActions . WipeRef )
if err != nil {
return nil , ctxerr . Wrap ( ctx , err , "get wipe reference script result" )
}
status . WipeScript = hsr
}
}
2024-02-13 18:03:53 +00:00
}
2024-02-26 16:31:00 +00:00
2024-02-13 18:03:53 +00:00
return status , nil
}
2024-02-26 16:31:00 +00:00
func ( ds * Datastore ) getHostMDMWindowsCommand ( ctx context . Context , cmdUUID , hostUUID string ) ( * fleet . MDMCommand , * fleet . MDMCommandResult , error ) {
cmd , err := ds . getMDMCommand ( ctx , ds . reader ( ctx ) , cmdUUID )
if err != nil {
return nil , nil , ctxerr . Wrap ( ctx , err , "get Windows MDM command" )
}
// get the MDM command result, which may be not found (indicating the command
// is pending). Note that it doesn't return ErrNoRows if not found, it
// returns success and an empty cmdRes slice.
cmdResults , err := ds . GetMDMWindowsCommandResults ( ctx , cmdUUID )
if err != nil {
return nil , nil , ctxerr . Wrap ( ctx , err , "get Windows MDM command result" )
}
// each item in the slice returned by GetMDMWindowsCommandResults is
// potentially a result for a different host, we need to find the one for
// that specific host.
var cmdRes * fleet . MDMCommandResult
for _ , r := range cmdResults {
if r . HostUUID != hostUUID {
continue
}
// all statuses for Windows indicate end of processing of the command
// (there is no equivalent of "NotNow" or "Idle" as for Apple).
cmdRes = r
break
}
return cmd , cmdRes , nil
}
func ( ds * Datastore ) getHostMDMAppleCommand ( ctx context . Context , cmdUUID , hostUUID string ) ( * fleet . MDMCommand , * fleet . MDMCommandResult , error ) {
cmd , err := ds . getMDMCommand ( ctx , ds . reader ( ctx ) , cmdUUID )
if err != nil {
return nil , nil , ctxerr . Wrap ( ctx , err , "get Apple MDM command" )
}
// get the MDM command result, which may be not found (indicating the command
// is pending). Note that it doesn't return ErrNoRows if not found, it
// returns success and an empty cmdRes slice.
cmdResults , err := ds . GetMDMAppleCommandResults ( ctx , cmdUUID )
if err != nil {
return nil , nil , ctxerr . Wrap ( ctx , err , "get Apple MDM command result" )
}
// each item in the slice returned by GetMDMAppleCommandResults is
// potentially a result for a different host, we need to find the one for
// that specific host.
var cmdRes * fleet . MDMCommandResult
for _ , r := range cmdResults {
if r . HostUUID != hostUUID {
continue
}
if r . Status == fleet . MDMAppleStatusAcknowledged || r . Status == fleet . MDMAppleStatusError || r . Status == fleet . MDMAppleStatusCommandFormatError {
cmdRes = r
break
}
}
return cmd , cmdRes , nil
}
2024-02-13 18:03:53 +00:00
// LockHostViaScript will create the script execution request and update
// host_mdm_actions in a single transaction.
2024-02-26 21:52:23 +00:00
func ( ds * Datastore ) LockHostViaScript ( ctx context . Context , request * fleet . HostScriptRequestPayload , hostFleetPlatform string ) error {
2024-02-13 18:03:53 +00:00
var res * fleet . HostScriptResult
return ds . withRetryTxx ( ctx , func ( tx sqlx . ExtContext ) error {
var err error
2024-03-04 16:00:08 +00:00
2024-07-29 15:56:01 +00:00
scRes , err := insertScriptContents ( ctx , tx , request . ScriptContents )
2024-03-04 16:00:08 +00:00
if err != nil {
return err
}
id , _ := scRes . LastInsertId ( )
2024-10-18 17:38:26 +00:00
request . ScriptContentID = uint ( id ) //nolint:gosec // dismiss G115
2024-03-04 16:00:08 +00:00
2024-07-29 15:56:01 +00:00
res , err = newHostScriptExecutionRequest ( ctx , tx , request )
2024-02-13 18:03:53 +00:00
if err != nil {
return ctxerr . Wrap ( ctx , err , "lock host via script create execution" )
}
// on duplicate we don't clear any other existing state because at this
// point in time, this is just a request to lock the host that is recorded,
// it is pending execution. The host's state should be updated to "locked"
// only when the script execution is successfully completed, and then any
// unlock or wipe references should be cleared.
const stmt = `
INSERT INTO host_mdm_actions
(
host_id ,
lock_ref ,
2024-02-26 21:52:23 +00:00
fleet_platform
2024-02-13 18:03:53 +00:00
)
2024-02-26 21:52:23 +00:00
VALUES ( ? , ? , ? )
2024-02-13 18:03:53 +00:00
ON DUPLICATE KEY UPDATE
lock_ref = VALUES ( lock_ref )
`
_ , err = tx . ExecContext ( ctx , stmt ,
request . HostID ,
res . ExecutionID ,
2024-02-26 21:52:23 +00:00
hostFleetPlatform ,
2024-02-13 18:03:53 +00:00
)
if err != nil {
return ctxerr . Wrap ( ctx , err , "lock host via script update mdm actions" )
}
return nil
} )
}
// UnlockHostViaScript will create the script execution request and update
// host_mdm_actions in a single transaction.
2024-02-26 21:52:23 +00:00
func ( ds * Datastore ) UnlockHostViaScript ( ctx context . Context , request * fleet . HostScriptRequestPayload , hostFleetPlatform string ) error {
2024-02-13 18:03:53 +00:00
var res * fleet . HostScriptResult
return ds . withRetryTxx ( ctx , func ( tx sqlx . ExtContext ) error {
var err error
2024-03-04 16:00:08 +00:00
2024-07-29 15:56:01 +00:00
scRes , err := insertScriptContents ( ctx , tx , request . ScriptContents )
2024-03-04 16:00:08 +00:00
if err != nil {
return err
}
id , _ := scRes . LastInsertId ( )
2024-10-18 17:38:26 +00:00
request . ScriptContentID = uint ( id ) //nolint:gosec // dismiss G115
2024-03-04 16:00:08 +00:00
2024-07-29 15:56:01 +00:00
res , err = newHostScriptExecutionRequest ( ctx , tx , request )
2024-02-13 18:03:53 +00:00
if err != nil {
return ctxerr . Wrap ( ctx , err , "unlock host via script create execution" )
}
// on duplicate we don't clear any other existing state because at this
// point in time, this is just a request to unlock the host that is
// recorded, it is pending execution. The host's state should be updated to
// "unlocked" only when the script execution is successfully completed, and
// then any lock or wipe references should be cleared.
const stmt = `
INSERT INTO host_mdm_actions
(
host_id ,
unlock_ref ,
2024-02-26 21:52:23 +00:00
fleet_platform
2024-02-13 18:03:53 +00:00
)
2024-02-26 21:52:23 +00:00
VALUES ( ? , ? , ? )
2024-02-13 18:03:53 +00:00
ON DUPLICATE KEY UPDATE
unlock_ref = VALUES ( unlock_ref ) ,
unlock_pin = NULL
`
_ , err = tx . ExecContext ( ctx , stmt ,
request . HostID ,
res . ExecutionID ,
2024-02-26 21:52:23 +00:00
hostFleetPlatform ,
2024-02-13 18:03:53 +00:00
)
if err != nil {
return ctxerr . Wrap ( ctx , err , "unlock host via script update mdm actions" )
}
return err
} )
}
2024-02-26 16:31:00 +00:00
// WipeHostViaScript creates the script execution request and updates the
// host_mdm_actions table in a single transaction.
2024-02-26 21:52:23 +00:00
func ( ds * Datastore ) WipeHostViaScript ( ctx context . Context , request * fleet . HostScriptRequestPayload , hostFleetPlatform string ) error {
2024-02-26 16:31:00 +00:00
var res * fleet . HostScriptResult
return ds . withRetryTxx ( ctx , func ( tx sqlx . ExtContext ) error {
var err error
2024-03-05 13:51:57 +00:00
2024-07-29 15:56:01 +00:00
scRes , err := insertScriptContents ( ctx , tx , request . ScriptContents )
2024-03-05 13:51:57 +00:00
if err != nil {
return err
}
id , _ := scRes . LastInsertId ( )
2024-10-18 17:38:26 +00:00
request . ScriptContentID = uint ( id ) //nolint:gosec // dismiss G115
2024-03-05 13:51:57 +00:00
2024-07-29 15:56:01 +00:00
res , err = newHostScriptExecutionRequest ( ctx , tx , request )
2024-02-26 16:31:00 +00:00
if err != nil {
return ctxerr . Wrap ( ctx , err , "wipe host via script create execution" )
}
// on duplicate we don't clear any other existing state because at this
// point in time, this is just a request to wipe the host that is recorded,
// it is pending execution, so if it was locked, it is still locked (so the
// lock_ref info must still be there).
const stmt = `
INSERT INTO host_mdm_actions
(
host_id ,
2024-02-26 21:52:23 +00:00
wipe_ref ,
fleet_platform
2024-02-26 16:31:00 +00:00
)
2024-02-26 21:52:23 +00:00
VALUES ( ? , ? , ? )
2024-02-26 16:31:00 +00:00
ON DUPLICATE KEY UPDATE
wipe_ref = VALUES ( wipe_ref )
`
_ , err = tx . ExecContext ( ctx , stmt ,
request . HostID ,
res . ExecutionID ,
2024-02-26 21:52:23 +00:00
hostFleetPlatform ,
2024-02-26 16:31:00 +00:00
)
if err != nil {
return ctxerr . Wrap ( ctx , err , "wipe host via script update mdm actions" )
}
return err
} )
}
2024-02-26 21:52:23 +00:00
func ( ds * Datastore ) UnlockHostManually ( ctx context . Context , hostID uint , hostFleetPlatform string , ts time . Time ) error {
2024-02-13 18:03:53 +00:00
const stmt = `
INSERT INTO host_mdm_actions
(
host_id ,
2024-02-26 21:52:23 +00:00
unlock_ref ,
fleet_platform
2024-02-13 18:03:53 +00:00
)
2024-02-26 21:52:23 +00:00
VALUES ( ? , ? , ? )
2024-02-13 18:03:53 +00:00
ON DUPLICATE KEY UPDATE
-- do not overwrite if a value is already set
unlock_ref = IF ( unlock_ref IS NULL , VALUES ( unlock_ref ) , unlock_ref )
`
// for macOS, the unlock_ref is just the timestamp at which the user first
// requested to unlock the host. This then indicates in the host's status
// that it's pending an unlock (which requires manual intervention by
// entering a PIN on the device). The /unlock endpoint can be called multiple
// times, so we record the timestamp of the first time it was requested and
// from then on, the host is marked as "pending unlock" until the device is
2024-02-26 16:31:00 +00:00
// actually unlocked with the PIN. The actual unlocking happens when the
// device sends an Idle MDM request.
2024-02-13 18:03:53 +00:00
unlockRef := ts . Format ( time . DateTime )
2024-02-26 21:52:23 +00:00
_ , err := ds . writer ( ctx ) . ExecContext ( ctx , stmt , hostID , unlockRef , hostFleetPlatform )
2024-02-13 18:03:53 +00:00
return ctxerr . Wrap ( ctx , err , "record manual unlock host request" )
}
2024-06-14 11:58:17 +00:00
func buildHostLockWipeStatusUpdateStmt ( refCol string , succeeded bool , joinPart string , setUnlockRef bool ) string {
2024-02-26 16:31:00 +00:00
var alias string
stmt := ` UPDATE host_mdm_actions `
if joinPart != "" {
stmt += ` hma ` + joinPart
alias = "hma."
}
stmt += ` SET `
2024-02-13 18:03:53 +00:00
if succeeded {
switch refCol {
case "lock_ref" :
// Note that this must not clear the unlock_pin, because recording the
// lock request does generate the PIN and store it there to be used by an
// eventual unlock.
2024-06-14 11:58:17 +00:00
if ! setUnlockRef {
2024-06-14 12:08:54 +00:00
stmt += fmt . Sprintf ( "%sunlock_ref = NULL, %[1]swipe_ref = NULL" , alias )
} else {
2024-06-14 11:58:17 +00:00
// Currently only used for Apple MDM devices.
// We set the unlock_ref to current time since the device can be unlocked any time after the lock.
// Apple MDM does not have a concept of unlock pending.
stmt += fmt . Sprintf ( "%sunlock_ref = '%s', %[1]swipe_ref = NULL" , alias , time . Now ( ) . Format ( time . DateTime ) )
}
2024-02-13 18:03:53 +00:00
case "unlock_ref" :
// a successful unlock clears itself as well as the lock ref, because
// unlock is the default state so we don't need to keep its unlock_ref
// around once it's confirmed.
2024-02-26 16:31:00 +00:00
stmt += fmt . Sprintf ( "%slock_ref = NULL, %[1]sunlock_ref = NULL, %[1]sunlock_pin = NULL, %[1]swipe_ref = NULL" , alias )
2024-02-13 18:03:53 +00:00
case "wipe_ref" :
2024-02-26 16:31:00 +00:00
stmt += fmt . Sprintf ( "%slock_ref = NULL, %[1]sunlock_ref = NULL, %[1]sunlock_pin = NULL" , alias )
2024-02-13 18:03:53 +00:00
}
} else {
// if the action failed, then we clear the reference to that action itself so
// the host stays in the previous state (it doesn't transition to the new
// state).
2024-02-26 16:31:00 +00:00
stmt += fmt . Sprintf ( "%s" + refCol + " = NULL" , alias )
2024-02-13 18:03:53 +00:00
}
2024-02-26 16:31:00 +00:00
return stmt
}
func ( ds * Datastore ) UpdateHostLockWipeStatusFromAppleMDMResult ( ctx context . Context , hostUUID , cmdUUID , requestType string , succeeded bool ) error {
// a bit of MDM protocol leaking in the mysql layer, but it's either that or
// the other way around (MDM protocol would translate to database column)
var refCol string
2024-06-14 11:58:17 +00:00
var setUnlockRef bool
2024-02-26 16:31:00 +00:00
switch requestType {
case "EraseDevice" :
refCol = "wipe_ref"
case "DeviceLock" :
refCol = "lock_ref"
2024-06-14 11:58:17 +00:00
setUnlockRef = true
2024-02-26 16:31:00 +00:00
default :
return nil
}
2024-06-14 11:58:17 +00:00
return updateHostLockWipeStatusFromResultAndHostUUID ( ctx , ds . writer ( ctx ) , hostUUID , refCol , cmdUUID , succeeded , setUnlockRef )
2024-02-26 16:31:00 +00:00
}
2024-06-14 11:58:17 +00:00
func updateHostLockWipeStatusFromResultAndHostUUID (
ctx context . Context , tx sqlx . ExtContext , hostUUID , refCol , cmdUUID string , succeeded bool , setUnlockRef bool ,
) error {
stmt := buildHostLockWipeStatusUpdateStmt ( refCol , succeeded , ` JOIN hosts h ON hma.host_id = h.id ` , setUnlockRef )
2024-02-26 16:31:00 +00:00
stmt += ` WHERE h.uuid = ? AND hma. ` + refCol + ` = ? `
_ , err := tx . ExecContext ( ctx , stmt , hostUUID , cmdUUID )
return ctxerr . Wrap ( ctx , err , "update host lock/wipe status from result via host uuid" )
}
func updateHostLockWipeStatusFromResult ( ctx context . Context , tx sqlx . ExtContext , hostID uint , refCol string , succeeded bool ) error {
2024-06-14 11:58:17 +00:00
stmt := buildHostLockWipeStatusUpdateStmt ( refCol , succeeded , "" , false )
2024-02-26 16:31:00 +00:00
stmt += ` WHERE host_id = ? `
2024-02-13 18:03:53 +00:00
_ , err := tx . ExecContext ( ctx , stmt , hostID )
return ctxerr . Wrap ( ctx , err , "update host lock/wipe status from result" )
}
2024-03-13 19:53:08 +00:00
2024-09-05 19:20:36 +00:00
func updateUninstallStatusFromResult ( ctx context . Context , tx sqlx . ExtContext , hostID uint , executionID string , exitCode int ) error {
stmt := `
UPDATE host_software_installs SET uninstall_script_exit_code = ? WHERE execution_id = ? AND host_id = ?
`
if _ , err := tx . ExecContext ( ctx , stmt , exitCode , executionID , hostID ) ; err != nil {
return ctxerr . Wrap ( ctx , err , "update uninstall status from result" )
}
return nil
}
2024-03-13 19:53:08 +00:00
func ( ds * Datastore ) CleanupUnusedScriptContents ( ctx context . Context ) error {
deleteStmt := `
DELETE FROM
script_contents
WHERE
NOT EXISTS (
SELECT 1 FROM host_script_results WHERE script_content_id = script_contents . id )
AND NOT EXISTS (
SELECT 1 FROM scripts WHERE script_content_id = script_contents . id )
2024-06-10 19:03:34 +00:00
AND NOT EXISTS (
SELECT 1 FROM software_installers si
2024-09-05 19:20:36 +00:00
WHERE script_contents . id IN ( si . install_script_content_id , si . post_install_script_content_id , si . uninstall_script_content_id )
2024-09-09 15:45:38 +00:00
)
AND NOT EXISTS (
SELECT 1 FROM fleet_library_apps fla
WHERE script_contents . id IN ( fla . install_script_content_id , fla . uninstall_script_content_id )
2024-06-10 19:03:34 +00:00
)
2024-03-13 19:53:08 +00:00
`
_ , err := ds . writer ( ctx ) . ExecContext ( ctx , deleteStmt )
if err != nil {
return ctxerr . Wrap ( ctx , err , "cleaning up unused script contents" )
}
return nil
}
2024-05-02 13:20:54 +00:00
func ( ds * Datastore ) getOrGenerateScriptContentsID ( ctx context . Context , contents string ) ( uint , error ) {
csum := md5ChecksumScriptContent ( contents )
scriptContentsID , err := ds . optimisticGetOrInsert ( ctx ,
& parameterizedStmt {
Statement : ` SELECT id FROM script_contents WHERE md5_checksum = UNHEX(?) ` ,
Args : [ ] interface { } { csum } ,
} ,
& parameterizedStmt {
Statement : ` INSERT INTO script_contents (md5_checksum, contents) VALUES (UNHEX(?), ?) ` ,
Args : [ ] interface { } { csum , contents } ,
} ,
)
if err != nil {
return 0 , err
}
return scriptContentsID , nil
}