mirror of
https://github.com/fleetdm/fleet
synced 2026-05-16 21:48:48 +00:00
for #31536
# Details
This PR adds a new API as specced in [the API
PR](9bf150580b/docs/REST%20API/rest-api.md (list-hosts-targeted-in-batch-script))
for scheduled scripts.
# Checklist for submitter
If some of the following don't apply, delete the relevant line.
- [X] Changes file added for user-visible changes in `changes/`,
`orbit/changes/` or `ee/fleetd-chrome/changes`.
See [Changes
files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/guides/committing-changes.md#changes-files)
for more information.
- [X] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements)
## Testing
- [X] Added/updated automated tests
- [X] Where appropriate, [automated tests simulate multiple hosts and
test for host
isolation](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/reference/patterns-backend.md#unit-testing)
(updates to one hosts's records do not affect another)
- [X] QA'd all new/changed functionality manually
ran a batch script on 100 hosts and ran the API in Postman for each
status, then canceled the batch and ran the API to check the canceled
status.
---------
Co-authored-by: Lucas Manuel Rodriguez <lucas@fleetdm.com>
672 lines
25 KiB
Go
672 lines
25 KiB
Go
package fleet
|
|
|
|
import (
|
|
"bufio"
|
|
"errors"
|
|
"fmt"
|
|
"path/filepath"
|
|
"regexp"
|
|
"strings"
|
|
"time"
|
|
"unicode/utf8"
|
|
|
|
"github.com/fleetdm/fleet/v4/pkg/scripts"
|
|
)
|
|
|
|
// Script represents a saved script that can be executed on a host.
|
|
type Script struct {
|
|
ID uint `json:"id" db:"id"`
|
|
TeamID *uint `json:"team_id" db:"team_id"`
|
|
Name string `json:"name" db:"name"`
|
|
// ScriptContents is not returned in payloads nor is it returned
|
|
// from reading from the database, it is only used as payload to
|
|
// create the script. This is so that we minimize the number of
|
|
// times this potentially large field is transferred.
|
|
ScriptContents string `json:"-"`
|
|
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
|
// UpdatedAt serves as the "uploaded at" timestamp, since it is updated each
|
|
// time the script record gets updated.
|
|
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
|
|
// ScriptContentID is the ID of the script contents, which are stored separately from the Script.
|
|
ScriptContentID uint `json:"-" db:"script_content_id"`
|
|
}
|
|
|
|
func (s Script) AuthzType() string {
|
|
return "script"
|
|
}
|
|
|
|
func (s *Script) ValidateNewScript() error {
|
|
if s.Name == "" {
|
|
return errors.New("The file name must not be empty.")
|
|
}
|
|
if filepath.Ext(s.Name) != ".sh" && filepath.Ext(s.Name) != ".ps1" {
|
|
return errors.New("File type not supported. Only .sh and .ps1 file type is allowed.")
|
|
}
|
|
|
|
// validate the script contents as if it were already a saved script
|
|
if err := ValidateHostScriptContents(s.ScriptContents, true); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// HostScriptDetail represents the details of a script that applies to a specific host.
|
|
type HostScriptDetail struct {
|
|
// HostID is the ID of the host.
|
|
HostID uint `json:"-"`
|
|
// ScriptID is the ID of the script.
|
|
ScriptID uint `json:"script_id"`
|
|
// Name is the name of the script.
|
|
Name string `json:"name"`
|
|
// LastExecution is the most recent execution of the script on the host. It is nil if the script
|
|
// has never executed on the host.
|
|
LastExecution *HostScriptExecution `json:"last_execution"`
|
|
}
|
|
|
|
// NewHostScriptDetail creates a new HostScriptDetail and sets its LastExecution field based on the
|
|
// provided details.
|
|
func NewHostScriptDetail(hostID, scriptID uint, name string, executionID *string, executedAt *time.Time, exitCode *int64, hsrID *uint) *HostScriptDetail {
|
|
hs := HostScriptDetail{
|
|
HostID: hostID,
|
|
ScriptID: scriptID,
|
|
Name: name,
|
|
}
|
|
hs.setLastExecution(executionID, executedAt, exitCode, hsrID)
|
|
return &hs
|
|
}
|
|
|
|
// HostScriptExecution represents a single execution of a script on a host.
|
|
type HostScriptExecution struct {
|
|
// HostID is the ID of the host.
|
|
HostID uint `json:"-"`
|
|
// ScriptID is the ID of the script.
|
|
ScriptID uint `json:"-"`
|
|
// HSRID is the unique row identifier of the host_script_results table for this execution.
|
|
HSRID uint `json:"-"`
|
|
// ExecutionID is a unique identifier for a single execution of the script.
|
|
ExecutionID string `json:"execution_id"`
|
|
// ExecutedAt represents the time that the script was executed on the host. It should correspond to
|
|
// the created_at field of the host_script_results table for the associated HSRID.
|
|
ExecutedAt time.Time `json:"executed_at"`
|
|
// Status is the status of the script execution. It is one of "pending", "ran", or "error". It
|
|
// is derived from the exit_code field of the host_script_results table for the associated HSRID.
|
|
Status string `json:"status"`
|
|
}
|
|
|
|
// SetLastExecution updates the LastExecution field of the HostScriptDetail if the provided details
|
|
// are more recent than the current LastExecution. It returns true if the LastExecution was updated.
|
|
func (hs *HostScriptDetail) setLastExecution(executionID *string, executedAt *time.Time, exitCode *int64, hsrID *uint) bool {
|
|
if executionID == nil || executedAt == nil {
|
|
// no new execution, nothing to do
|
|
return false
|
|
}
|
|
|
|
newHSE := &HostScriptExecution{
|
|
ExecutionID: *executionID,
|
|
ExecutedAt: *executedAt,
|
|
}
|
|
if hsrID != nil {
|
|
newHSE.HSRID = *hsrID
|
|
}
|
|
switch {
|
|
case exitCode == nil:
|
|
newHSE.Status = "pending"
|
|
case *exitCode == 0:
|
|
newHSE.Status = "ran"
|
|
default:
|
|
newHSE.Status = "error"
|
|
}
|
|
|
|
if hs.LastExecution == nil {
|
|
// no previous execution, use the new one
|
|
hs.LastExecution = newHSE
|
|
return true
|
|
}
|
|
if newHSE.ExecutedAt.After(hs.LastExecution.ExecutedAt) {
|
|
// new execution is more recent, use it
|
|
hs.LastExecution = newHSE
|
|
return true
|
|
}
|
|
if newHSE.ExecutedAt == hs.LastExecution.ExecutedAt && newHSE.HSRID > hs.LastExecution.HSRID {
|
|
// same execution time, but new execution has a higher ID, use it
|
|
hs.LastExecution = newHSE
|
|
return true
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
type HostScriptRequestPayload struct {
|
|
HostID uint `json:"host_id"`
|
|
ScriptID *uint `json:"script_id"`
|
|
PolicyID *uint `json:"policy_id"`
|
|
ScriptContents string `json:"script_contents"`
|
|
ScriptContentID uint `json:"-"`
|
|
ScriptName string `json:"script_name"`
|
|
TeamID uint `json:"team_id,omitempty"`
|
|
// UserID is filled automatically from the context's user (the authenticated
|
|
// user that made the API request).
|
|
UserID *uint `json:"-"`
|
|
// SyncRequest is filled automatically based on the endpoint used to create
|
|
// the execution request (synchronous or asynchronous).
|
|
SyncRequest bool `json:"-"`
|
|
// SetupExperienceScriptID is the ID of the setup experience script related to this request
|
|
// payload, if such a script exists.
|
|
SetupExperienceScriptID *uint `json:"-"`
|
|
}
|
|
|
|
// Priority returns the priority to assign to this activity in the upcoming
|
|
// activities queue. It is the default priority except when the script is part
|
|
// of the setup experience flow.
|
|
func (r HostScriptRequestPayload) Priority() int {
|
|
if r.SetupExperienceScriptID != nil {
|
|
return 100
|
|
}
|
|
return 0
|
|
}
|
|
|
|
func (r HostScriptRequestPayload) ValidateParams(waitForResult time.Duration) error {
|
|
if r.ScriptContents == "" && r.ScriptID == nil && r.ScriptName == "" {
|
|
return NewInvalidArgumentError("script", `One of 'script_id', 'script_contents', or 'script_name' is required.`)
|
|
}
|
|
|
|
if r.ScriptID != nil {
|
|
switch {
|
|
case r.ScriptContents != "":
|
|
return NewInvalidArgumentError("script_id", `Only one of 'script_id' or 'script_contents' is allowed.`)
|
|
case r.ScriptName != "":
|
|
return NewInvalidArgumentError("script_id", `Only one of 'script_id' or 'script_name' is allowed.`)
|
|
case r.TeamID > 0:
|
|
return NewInvalidArgumentError("script_id", `Only one of 'script_id' or 'team_id' is allowed.`)
|
|
}
|
|
}
|
|
if r.ScriptContents != "" {
|
|
switch {
|
|
case r.ScriptName != "":
|
|
return NewInvalidArgumentError("script_contents", `Only one of 'script_contents' or 'script_name' is allowed.`)
|
|
case r.TeamID > 0:
|
|
return NewInvalidArgumentError("script_contents", `"Only one of 'script_contents' or 'team_id' is allowed.`)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
type HostScriptResultPayload struct {
|
|
HostID uint `json:"host_id"`
|
|
ExecutionID string `json:"execution_id"`
|
|
Output string `json:"output"`
|
|
Runtime int `json:"runtime"`
|
|
ExitCode int `json:"exit_code"`
|
|
Timeout int `json:"timeout"`
|
|
}
|
|
|
|
// HostScriptResult represents a script result that was requested to execute on
|
|
// a specific host. If no result was received yet for a script, the ExitCode
|
|
// field is null and the output is empty.
|
|
type HostScriptResult struct {
|
|
// ID is the unique row identifier of the host script result.
|
|
ID uint `json:"-" db:"id"`
|
|
// HostID is the host on which the script was executed.
|
|
HostID uint `json:"host_id" db:"host_id"`
|
|
// ExecutionID is a unique identifier for a single execution of the script.
|
|
ExecutionID string `json:"execution_id" db:"execution_id"`
|
|
// BatchExecutionID is an identifier that links this execution to a larger batch job
|
|
BatchExecutionID *string `json:"batch_execution_id" db:"batch_execution_id"`
|
|
// ScriptContents is the content of the script to execute.
|
|
ScriptContents string `json:"script_contents" db:"script_contents"`
|
|
// Output is the combined stdout/stderr output of the script. It is empty
|
|
// if no result was received yet.
|
|
Output string `json:"output" db:"output"`
|
|
// Runtime is the running time of the script in seconds, rounded.
|
|
Runtime int `json:"runtime" db:"runtime"`
|
|
// ExitCode is null if script execution result was never received from the
|
|
// host. It is -1 if it was received but the script did not terminate
|
|
// normally (same as how Go handles this: https://pkg.go.dev/os#ProcessState.ExitCode)
|
|
ExitCode *int64 `json:"exit_code" db:"exit_code"`
|
|
// Timeout is the maximum time in seconds that the script was allowed to run
|
|
// at the time of execution.
|
|
Timeout *int `json:"timeout" db:"timeout"`
|
|
// CreatedAt is the creation timestamp of the script execution request. It is
|
|
// not returned as part of the payloads, but is used to determine if the script
|
|
// is too old to still expect a response from the host.
|
|
CreatedAt time.Time `json:"-" db:"created_at"`
|
|
// ScriptID is the id of the saved script to execute, or nil if this was an
|
|
// anonymous script execution.
|
|
ScriptID *uint `json:"script_id" db:"script_id"`
|
|
// PolicyID is the id of the policy that triggered the script execution, or
|
|
// nil if the execution was not triggered by a policy failure
|
|
PolicyID *uint `json:"policy_id" db:"policy_id"`
|
|
// UserID is the id of the user that requested execution. It is not part of
|
|
// the rendered JSON as it is only returned by the
|
|
// /hosts/:id/activities/upcoming endpoint which doesn't use this struct as
|
|
// return type.
|
|
UserID *uint `json:"-" db:"user_id"`
|
|
// SyncRequest is used to determine when creating the script ran activity if
|
|
// the request was synchronous or asynchronous. It is otherwise not returned
|
|
// as part of any API endpoint.
|
|
SyncRequest bool `json:"-" db:"sync_request"`
|
|
|
|
// TeamID is only used for authorization, it must be set to the team id of
|
|
// the host when checking authorization and is otherwise not set.
|
|
TeamID *uint `json:"team_id" db:"-"` // TODO: should we omit this from the json result?
|
|
|
|
// Message is the UserMessage associated with a response from an execution.
|
|
// It may be set by the endpoint and included in the resulting JSON but it is
|
|
// not otherwise part of the host_script_results table.
|
|
Message string `json:"message" db:"-"`
|
|
|
|
// Hostname can be set by the endpoint as extra information to make available
|
|
// when generating the UserMessage associated with a response from an
|
|
// execution. It is otherwise not part of the host_script_results table and
|
|
// not returned as part of the resulting JSON.
|
|
Hostname string `json:"-" db:"-"`
|
|
|
|
// HostDeletedAt indicates if the results are associated with a deleted host.
|
|
// This supports the soft-delete feature for script results so that the
|
|
// results can still be returned to see activity details after the host got
|
|
// deleted.
|
|
HostDeletedAt *time.Time `json:"-" db:"host_deleted_at"`
|
|
|
|
// SetupExperienceScriptID is the ID of the setup experience script, if this script execution
|
|
// was part of setup experience.
|
|
SetupExperienceScriptID *uint `json:"-" db:"setup_experience_script_id"`
|
|
|
|
// Canceled indicates if that script execution request was canceled by a
|
|
// user.
|
|
Canceled bool `json:"-" db:"canceled"`
|
|
}
|
|
|
|
func (hsr HostScriptResult) AuthzType() string {
|
|
return "host_script_result"
|
|
}
|
|
|
|
type BatchScriptHost struct {
|
|
// ID is the host on which the script was executed.
|
|
ID uint `json:"id" db:"id"`
|
|
// Display name is the host's display name.
|
|
DisplayName string `json:"display_name" db:"display_name"`
|
|
// ExecutionID is a unique identifier for a single execution of the script.
|
|
ScriptExecutionID string `json:"script_execution_id" db:"execution_id"`
|
|
// Output is the combined stdout/stderr output of the script. It is empty
|
|
// if no result was received yet.
|
|
ScriptOutput string `json:"script_output_preview,omitempty" db:"output"`
|
|
// Executed at is the time the script was executed on the host (if at all).
|
|
ScriptExecutedAt *time.Time `json:"script_executed_at,omitempty" db:"updated_at"`
|
|
// Status is the status of the host's batch script run.
|
|
Status BatchScriptExecutionStatus `json:"script_status" db:"status"`
|
|
}
|
|
|
|
// UserMessage returns the user-friendly message to associate with the current
|
|
// state of the HostScriptResult. This is returned as part of the API endpoints
|
|
// for running a script synchronously (so that fleetctl can display it) and to
|
|
// get the script results for an execution ID (e.g. when looking at the details
|
|
// screen of a script execution activity in the website).
|
|
func (hsr HostScriptResult) UserMessage(hostTimeout bool, hostTimeoutValue *int) string {
|
|
if hostTimeout {
|
|
return RunScriptHostTimeoutErrMsg
|
|
}
|
|
|
|
if hsr.ExitCode == nil {
|
|
if hsr.HostTimeout(scripts.MaxServerWaitTime) {
|
|
return RunScriptHostTimeoutErrMsg
|
|
}
|
|
|
|
if !hsr.SyncRequest {
|
|
return RunScriptAsyncScriptEnqueuedMsg
|
|
}
|
|
|
|
return RunScriptAlreadyRunningErrMsg
|
|
}
|
|
|
|
switch *hsr.ExitCode {
|
|
case -1:
|
|
return HostScriptTimeoutMessage(hostTimeoutValue)
|
|
case -2:
|
|
return RunScriptDisabledErrMsg
|
|
default:
|
|
return ""
|
|
}
|
|
}
|
|
|
|
func HostScriptTimeoutMessage(seconds *int) string {
|
|
var timeout int
|
|
if seconds == nil || *seconds == 0 {
|
|
timeout = int(scripts.MaxHostExecutionTime.Seconds())
|
|
} else {
|
|
timeout = *seconds
|
|
}
|
|
return fmt.Sprintf("Timeout. Fleet stopped the script after %d seconds to protect host performance.", timeout)
|
|
}
|
|
|
|
func (hsr HostScriptResult) HostTimeout(waitForResultTime time.Duration) bool {
|
|
return hsr.SyncRequest && hsr.ExitCode == nil && time.Now().After(hsr.CreatedAt.Add(waitForResultTime))
|
|
}
|
|
|
|
const (
|
|
SavedScriptMaxRuneLen = 500000
|
|
UnsavedScriptMaxRuneLen = 10000
|
|
)
|
|
|
|
// anchored, so that it matches to the end of the line
|
|
var (
|
|
scriptHashbangValidation = regexp.MustCompile(`^#!\s*(:?/usr)?/bin/(ba|z)?sh(?:\s*|\s+.*)$`)
|
|
ErrUnsupportedInterpreter = errors.New(`Interpreter not supported. Shell scripts must run in "#!/bin/sh", "#!/bin/bash", or "#!/bin/zsh."`)
|
|
)
|
|
|
|
// ValidateShebang validates if we support a script, and whether we
|
|
// can execute it directly, or need to pass it to a shell interpreter.
|
|
func ValidateShebang(s string) (directExecute bool, err error) {
|
|
if strings.HasPrefix(s, "#!") {
|
|
// read the first line in a portable way
|
|
s := bufio.NewScanner(strings.NewReader(s))
|
|
// if a hashbang is present, it can only be `(/usr)/bin/sh`, `(/usr)/bin/bash`, `(/usr)/bin/zsh` for now
|
|
if s.Scan() && !scriptHashbangValidation.MatchString(s.Text()) {
|
|
return false, ErrUnsupportedInterpreter
|
|
}
|
|
return true, nil
|
|
}
|
|
return false, nil
|
|
}
|
|
|
|
func ValidateHostScriptContents(s string, isSavedScript bool) error {
|
|
if s == "" {
|
|
return errors.New("Script contents must not be empty.")
|
|
}
|
|
|
|
maxLen := SavedScriptMaxRuneLen
|
|
maxLenErrMsg := RunScripSavedMaxLenErrMsg
|
|
if !isSavedScript {
|
|
maxLen = UnsavedScriptMaxRuneLen
|
|
maxLenErrMsg = RunScripUnsavedMaxLenErrMsg
|
|
}
|
|
|
|
// look for the script length in bytes first, as rune counting a huge string
|
|
// can be expensive.
|
|
if len(s) > utf8.UTFMax*maxLen {
|
|
return errors.New(maxLenErrMsg)
|
|
}
|
|
|
|
// now that we know that the script is at most 4*maxScriptRuneLen bytes long,
|
|
// we can safely count the runes for a precise check.
|
|
if utf8.RuneCountInString(s) > maxLen {
|
|
return errors.New(maxLenErrMsg)
|
|
}
|
|
|
|
// script must be a "text file", but that's not so simple to validate, so we
|
|
// assume that if it is valid utf8 encoding, it is a text file (binary files
|
|
// will often have invalid utf8 byte sequences).
|
|
if !utf8.ValidString(s) {
|
|
return errors.New("Wrong data format. Only plain text allowed.")
|
|
}
|
|
|
|
if _, err := ValidateShebang(s); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
type ScriptPayload struct {
|
|
Name string `json:"name"`
|
|
ScriptContents []byte `json:"script_contents"`
|
|
}
|
|
|
|
type SoftwareInstallerPayload struct {
|
|
URL string `json:"url"`
|
|
PreInstallQuery string `json:"pre_install_query"`
|
|
InstallScript string `json:"install_script"`
|
|
UninstallScript string `json:"uninstall_script"`
|
|
PostInstallScript string `json:"post_install_script"`
|
|
SelfService bool `json:"self_service"`
|
|
FleetMaintained bool `json:"-"`
|
|
Filename string `json:"-"`
|
|
InstallDuringSetup *bool `json:"install_during_setup"` // if nil, do not change saved value, otherwise set it
|
|
LabelsIncludeAny []string `json:"labels_include_any"`
|
|
LabelsExcludeAny []string `json:"labels_exclude_any"`
|
|
// ValidatedLabels is a struct that contains the validated labels for the
|
|
// software installer. It is nil if the labels have not been validated.
|
|
ValidatedLabels *LabelIdentsWithScope
|
|
SHA256 string `json:"sha256"`
|
|
Categories []string `json:"categories"`
|
|
// This is to support FMAs
|
|
Slug *string `json:"slug"`
|
|
MaintainedApp *MaintainedApp `json:"-"`
|
|
}
|
|
|
|
type HostLockWipeStatus struct {
|
|
// HostFleetPlatform is the fleet-normalized platform of the host, i.e. the
|
|
// result of host.FleetPlatform().
|
|
HostFleetPlatform string
|
|
|
|
// macOS hosts use an MDM command to lock
|
|
LockMDMCommand *MDMCommand
|
|
LockMDMCommandResult *MDMCommandResult
|
|
|
|
// windows and linux hosts use a script to lock
|
|
LockScript *HostScriptResult
|
|
|
|
// macOS hosts must manually unlock using a secret PIN, which is stored here
|
|
// when the lock request is sent.
|
|
UnlockPIN string
|
|
// macOS records the timestamp of the unlock request in the "unlock_ref",
|
|
// which is then stored here.
|
|
UnlockRequestedAt time.Time
|
|
// windows and linux hosts use a script to unlock
|
|
UnlockScript *HostScriptResult
|
|
|
|
// macOS and Windows use MDM commands for Wipe
|
|
WipeMDMCommand *MDMCommand
|
|
WipeMDMCommandResult *MDMCommandResult
|
|
|
|
// Linux uses a script for Wipe
|
|
WipeScript *HostScriptResult
|
|
}
|
|
|
|
// ScriptResponse is the response type used when applying scripts by batch.
|
|
type ScriptResponse struct {
|
|
// TeamID is the id of the team.
|
|
// A value of nil means it is scoped to hosts that are assigned to "No team".
|
|
TeamID *uint `json:"team_id" db:"team_id"`
|
|
// ID is the id of the script
|
|
ID uint `json:"id" db:"id"`
|
|
// Name is the name of the script
|
|
Name string `json:"name" db:"name"`
|
|
}
|
|
|
|
type DeviceStatus string
|
|
|
|
const (
|
|
DeviceStatusWiped DeviceStatus = "wiped"
|
|
DeviceStatusLocked DeviceStatus = "locked"
|
|
DeviceStatusUnlocked DeviceStatus = "unlocked"
|
|
)
|
|
|
|
func (s HostLockWipeStatus) DeviceStatus() DeviceStatus {
|
|
switch {
|
|
case s.IsWiped():
|
|
return DeviceStatusWiped
|
|
case s.IsLocked():
|
|
return DeviceStatusLocked
|
|
default:
|
|
return DeviceStatusUnlocked
|
|
}
|
|
}
|
|
|
|
type PendingDeviceAction string
|
|
|
|
const (
|
|
PendingActionLock PendingDeviceAction = "lock"
|
|
PendingActionUnlock PendingDeviceAction = "unlock"
|
|
PendingActionWipe PendingDeviceAction = "wipe"
|
|
PendingActionNone PendingDeviceAction = ""
|
|
)
|
|
|
|
func (s HostLockWipeStatus) PendingAction() PendingDeviceAction {
|
|
switch {
|
|
case s.IsPendingLock():
|
|
return PendingActionLock
|
|
case s.IsPendingUnlock():
|
|
return PendingActionUnlock
|
|
case s.IsPendingWipe():
|
|
return PendingActionWipe
|
|
default:
|
|
return PendingActionNone
|
|
}
|
|
}
|
|
|
|
func (s *HostLockWipeStatus) IsPendingLock() bool {
|
|
if s.HostFleetPlatform == "darwin" || s.HostFleetPlatform == "ios" || s.HostFleetPlatform == "ipados" {
|
|
// pending lock if an MDM command is queued but no result received yet
|
|
return s.LockMDMCommand != nil && s.LockMDMCommandResult == nil
|
|
}
|
|
// pending lock if script execution request is queued but no result yet and not canceled
|
|
return s.LockScript != nil && s.LockScript.ExitCode == nil && !s.LockScript.Canceled
|
|
}
|
|
|
|
func (s HostLockWipeStatus) IsPendingUnlock() bool {
|
|
if s.HostFleetPlatform == "darwin" || s.HostFleetPlatform == "ios" || s.HostFleetPlatform == "ipados" {
|
|
// Apple MDM does not have a concept of pending unlock.
|
|
return false
|
|
}
|
|
// pending unlock if script execution request is queued but no result yet and not canceled
|
|
return s.UnlockScript != nil && s.UnlockScript.ExitCode == nil && !s.UnlockScript.Canceled
|
|
}
|
|
|
|
func (s HostLockWipeStatus) IsPendingWipe() bool {
|
|
if s.HostFleetPlatform == "linux" {
|
|
// pending wipe if script execution request is queued but no result yet and not canceled
|
|
return s.WipeScript != nil && s.WipeScript.ExitCode == nil && !s.WipeScript.Canceled
|
|
}
|
|
// pending wipe if an MDM command is queued but no result received yet
|
|
return s.WipeMDMCommand != nil && s.WipeMDMCommandResult == nil
|
|
}
|
|
|
|
func (s HostLockWipeStatus) IsLocked() bool {
|
|
// this state is regardless of pending unlock/wipe (it reports whether the
|
|
// host is locked *now*).
|
|
|
|
if s.HostFleetPlatform == "darwin" || s.HostFleetPlatform == "ios" || s.HostFleetPlatform == "ipados" {
|
|
// locked if an MDM command was sent and succeeded
|
|
return s.LockMDMCommand != nil && s.LockMDMCommandResult != nil &&
|
|
s.LockMDMCommandResult.Status == MDMAppleStatusAcknowledged
|
|
}
|
|
// locked if a script was sent and succeeded
|
|
return s.LockScript != nil && s.LockScript.ExitCode != nil &&
|
|
*s.LockScript.ExitCode == 0
|
|
}
|
|
|
|
func (s HostLockWipeStatus) IsUnlocked() bool {
|
|
// this state is regardless of pending lock/unlock/wipe (it reports whether
|
|
// the host is unlocked *now*).
|
|
return !s.IsLocked() && !s.IsWiped()
|
|
}
|
|
|
|
func (s HostLockWipeStatus) IsWiped() bool {
|
|
switch s.HostFleetPlatform {
|
|
case "linux":
|
|
// wiped if script was sent and succeeded
|
|
return s.WipeScript != nil && s.WipeScript.ExitCode != nil &&
|
|
*s.WipeScript.ExitCode == 0
|
|
case "windows":
|
|
// wiped if an MDM command was sent and succeeded
|
|
return s.WipeMDMCommand != nil && s.WipeMDMCommandResult != nil &&
|
|
strings.HasPrefix(s.WipeMDMCommandResult.Status, "2")
|
|
case "darwin", "ios", "ipados":
|
|
// wiped if an MDM command was sent and succeeded
|
|
return s.WipeMDMCommand != nil && s.WipeMDMCommandResult != nil &&
|
|
s.WipeMDMCommandResult.Status == MDMAppleStatusAcknowledged
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
var (
|
|
BatchExecuteIncompatiblePlatform = "incompatible-platform"
|
|
BatchExecuteIncompatibleFleetd = "incompatible-fleetd"
|
|
BatchExecuteInvalidHost = "invalid-host"
|
|
)
|
|
|
|
type BatchExecutionStatusFilter struct {
|
|
ScriptID *uint `json:"script_id,omitempty"`
|
|
TeamID *uint `json:"team_id,omitempty"` // if nil, it is scoped to hosts that are assigned to "No team"
|
|
Status *string `json:"status,omitempty"` // e.g. "pending", "ran", "errored", "canceled", "incompatible-platform", "incompatible-fleetd"
|
|
// ExecutionID is the unique identifier for a single execution of the script.
|
|
ExecutionID *string `json:"execution_id,omitempty"`
|
|
// Limit is the maximum number of results to return.
|
|
// If not set, it defaults to 100.
|
|
Limit *uint `json:"limit,omitempty"`
|
|
// Offset is the number of results to skip before returning results.
|
|
// If not set, it defaults to 0.
|
|
Offset *uint `json:"offset,omitempty"`
|
|
}
|
|
|
|
type BatchExecutionHost struct {
|
|
HostID uint `json:"host_id" db:"host_id"`
|
|
HostDisplayName string `json:"host_display_name" db:"hostname"`
|
|
ExecutionID *string `json:"execution_id,omitempty" db:"execution_id"`
|
|
Error *string `json:"error,omitempty" db:"error"`
|
|
}
|
|
|
|
type BatchActivity struct {
|
|
ID uint `json:"id" db:"id"`
|
|
BatchExecutionID string `json:"batch_execution_id" db:"execution_id"`
|
|
UserID *uint `json:"user_id" db:"user_id"`
|
|
JobID *uint `json:"-" db:"job_id"`
|
|
ActivityType BatchExecutionActivityType `json:"-" db:"activity_type"`
|
|
ScriptID *uint `json:"script_id" db:"script_id"`
|
|
ScriptName string `json:"script_name" db:"script_name"`
|
|
TeamID *uint `json:"team_id" db:"team_id"`
|
|
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
|
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
|
|
NotBefore *time.Time `json:"not_before,omitempty" db:"not_before"`
|
|
StartedAt *time.Time `json:"started_at,omitempty" db:"started_at"`
|
|
FinishedAt *time.Time `json:"finished_at,omitempty" db:"finished_at"`
|
|
Canceled bool `json:"canceled" db:"canceled"`
|
|
Status ScheduledBatchExecutionStatus `json:"status" db:"status"`
|
|
NumTargeted *uint `json:"targeted_host_count" db:"num_targeted"`
|
|
NumPending *uint `json:"pending_host_count" db:"num_pending"`
|
|
NumRan *uint `json:"ran_host_count" db:"num_ran"`
|
|
NumErrored *uint `json:"errored_host_count" db:"num_errored"`
|
|
NumCanceled *uint `json:"canceled_host_count" db:"num_canceled"`
|
|
NumIncompatible *uint `json:"incompatible_host_count" db:"num_incompatible"`
|
|
}
|
|
|
|
type BatchActivityHostResult struct {
|
|
ID uint `db:"id"`
|
|
BatchExecutionID string `db:"batch_execution_id"`
|
|
HostID uint `db:"host_id"`
|
|
HostExecutionID *string `db:"host_execution_id"`
|
|
Error *string `db:"error"`
|
|
}
|
|
|
|
type BatchActivityScriptJobArgs struct {
|
|
ExecutionID string `json:"execution_id"`
|
|
}
|
|
|
|
type ScheduledBatchExecutionStatus string
|
|
|
|
var (
|
|
ScheduledBatchExecutionStarted ScheduledBatchExecutionStatus = "started"
|
|
ScheduledBatchExecutionScheduled ScheduledBatchExecutionStatus = "scheduled"
|
|
ScheduledBatchExecutionFinished ScheduledBatchExecutionStatus = "finished"
|
|
)
|
|
|
|
type BatchExecutionActivityType string
|
|
|
|
var BatchExecutionActivityScript BatchExecutionActivityType = "script"
|
|
|
|
const BatchActivityScriptsJobName = "batch_scripts"
|
|
|
|
// ValidateScriptPlatform returns whether a script can run on a host based on its host.Platform
|
|
func ValidateScriptPlatform(scriptName, platform string) bool {
|
|
switch filepath.Ext(scriptName) {
|
|
case ".sh":
|
|
return IsUnixLike(platform)
|
|
case ".ps1":
|
|
return platform == "windows"
|
|
default:
|
|
return false
|
|
}
|
|
}
|