diff options
author | Bryan McNulty <bryanmcnulty@protonmail.com> | 2025-04-20 11:26:44 -0500 |
---|---|---|
committer | Bryan McNulty <bryanmcnulty@protonmail.com> | 2025-04-20 11:26:44 -0500 |
commit | ce79cf929133ea2592fb899d6339c1e299aa9eeb (patch) | |
tree | 964763333f623969febd5f1ef86a00862a83b591 /pkg | |
parent | 61578457eed9243d3be1bb120cce5995e149adec (diff) | |
download | goexec-ce79cf929133ea2592fb899d6339c1e299aa9eeb.tar.gz goexec-ce79cf929133ea2592fb899d6339c1e299aa9eeb.zip |
Added `tsch change` command
Diffstat (limited to 'pkg')
-rw-r--r-- | pkg/goexec/tsch/change.go | 152 | ||||
-rw-r--r-- | pkg/goexec/tsch/demand.go | 4 | ||||
-rw-r--r-- | pkg/goexec/tsch/module.go | 4 | ||||
-rw-r--r-- | pkg/goexec/tsch/task/action.go | 229 | ||||
-rw-r--r-- | pkg/goexec/tsch/task/misc.go | 90 | ||||
-rw-r--r-- | pkg/goexec/tsch/task/settings.go | 65 | ||||
-rw-r--r-- | pkg/goexec/tsch/task/task.go | 22 | ||||
-rw-r--r-- | pkg/goexec/tsch/task/trigger.go | 195 | ||||
-rw-r--r-- | pkg/goexec/tsch/tsch.go | 6 |
9 files changed, 760 insertions, 7 deletions
diff --git a/pkg/goexec/tsch/change.go b/pkg/goexec/tsch/change.go new file mode 100644 index 0000000..44877c9 --- /dev/null +++ b/pkg/goexec/tsch/change.go @@ -0,0 +1,152 @@ +package tschexec + +import ( + "context" + "encoding/xml" + "fmt" + "github.com/FalconOpsLLC/goexec/pkg/goexec" + "github.com/FalconOpsLLC/goexec/pkg/goexec/tsch/task" + "github.com/oiweiwei/go-msrpc/msrpc/tsch/itaskschedulerservice/v1" + "github.com/rs/zerolog" + "regexp" + "time" +) + +const ( + FlagTaskUpdate uint32 = 0b_00000000_00000000_00000000_00000100 + MethodChange = "Change" + DefaultWaitTime = 1 * time.Second +) + +type TschChange struct { + Tsch + goexec.Executor + goexec.Cleaner + + IO goexec.ExecutionIO + + WorkingDirectory string + NoStart bool + NoRevert bool + WaitTime time.Duration +} + +func (m *TschChange) Execute(ctx context.Context, execIO *goexec.ExecutionIO) (err error) { + + log := zerolog.Ctx(ctx).With(). + Str("module", ModuleName). + Str("method", MethodChange). + Str("task", m.TaskPath). + Logger() + + retrieveResponse, err := m.tsch.RetrieveTask(ctx, &itaskschedulerservice.RetrieveTaskRequest{ + Path: m.TaskPath, + }) + + if err != nil { + log.Error().Err(err).Msg("Failed to retrieve task") + return fmt.Errorf("retrieve task: %w", err) + } + if retrieveResponse.Return != 0 { + log.Error().Err(err).Str("code", fmt.Sprintf("0x%02x", retrieveResponse.Return)). + Msg("Failed to retrieve task") + return fmt.Errorf("retrieve task returned non-zero exit code: %02x", retrieveResponse.Return) + } + + log.Info().Msg("Successfully retrieved existing task definition") + log.Debug().Str("xml", retrieveResponse.XML).Msg("Got task definition") + + tk := task.Task{} + + enc := regexp.MustCompile(`(?i)^<\?xml .*?\?>`) + tkStr := enc.ReplaceAllString(retrieveResponse.XML, `<?xml version="1.0" encoding="utf-8"?>`) + + if err = xml.Unmarshal([]byte(tkStr), &tk); err != nil { + log.Error().Err(err).Msg("Failed to unmarshal task XML") + + return fmt.Errorf("unmarshal task XML: %w", err) + } + + cmd := execIO.CommandLine() + + tk.Actions.Exec = append(tk.Actions.Exec, task.ExecAction{ + Command: cmd[0], + Arguments: cmd[1], + WorkingDirectory: m.WorkingDirectory, + }) + + doc, err := xml.Marshal(tk) + + if err != nil { + log.Error().Err(err).Msg("failed to marshal task XML") + return fmt.Errorf("marshal task: %w", err) + } + + taskXml := TaskXmlHeader + string(doc) + log.Debug().Str("xml", taskXml).Msg("Serialized new task") + + registerResponse, err := m.tsch.RegisterTask(ctx, &itaskschedulerservice.RegisterTaskRequest{ + Path: m.TaskPath, + XML: taskXml, + Flags: FlagTaskUpdate, + }) + + if !m.NoRevert { + + m.AddCleaners(func(ctxInner context.Context) error { + + revertResponse, err := m.tsch.RegisterTask(ctx, &itaskschedulerservice.RegisterTaskRequest{ + Path: m.TaskPath, + XML: retrieveResponse.XML, + Flags: FlagTaskUpdate, + }) + + if err != nil { + return err + } + if revertResponse.Return != 0 { + return fmt.Errorf("revert task definition returned non-zero exit code: %02x", revertResponse.Return) + } + return nil + }) + } + + if err != nil { + log.Error().Err(err).Msg("Failed to update task") + + return fmt.Errorf("update task: %w", err) + } + if registerResponse.Return != 0 { + log.Error().Err(err).Str("code", fmt.Sprintf("0x%02x", registerResponse.Return)).Msg("Failed to update task definition") + + return fmt.Errorf("update task returned non-zero exit code: %02x", registerResponse.Return) + } + log.Info().Msg("Successfully updated task definition") + + if !m.NoStart { + + runResponse, err := m.tsch.Run(ctx, &itaskschedulerservice.RunRequest{ + Path: m.TaskPath, + }) + + if err != nil { + log.Error().Err(err).Msg("Failed to run modified task") + + return fmt.Errorf("run task: %w", err) + } + + if ret := uint32(runResponse.Return); ret != 0 { + log.Error().Str("code", fmt.Sprintf("0x%08x", ret)).Msg("Run task returned non-zero exit code") + + return fmt.Errorf("run task returned non-zero exit code: 0x%08x", ret) + } + + log.Info().Msg("Successfully started modified task") + } + + if m.WaitTime <= 0 { + m.WaitTime = DefaultWaitTime + } + time.Sleep(m.WaitTime) + return +} diff --git a/pkg/goexec/tsch/demand.go b/pkg/goexec/tsch/demand.go index c397453..74a41fe 100644 --- a/pkg/goexec/tsch/demand.go +++ b/pkg/goexec/tsch/demand.go @@ -24,7 +24,7 @@ type TschDemand struct { SessionId uint32 } -func (m *TschDemand) Execute(ctx context.Context, in *goexec.ExecutionIO) (err error) { +func (m *TschDemand) Execute(ctx context.Context, execIO *goexec.ExecutionIO) (err error) { log := zerolog.Ctx(ctx).With(). Str("module", ModuleName). @@ -39,7 +39,7 @@ func (m *TschDemand) Execute(ctx context.Context, in *goexec.ExecutionIO) (err e Hidden: !m.NotHidden, triggers: taskTriggers{}, }, - in, + execIO, ) if err != nil { return err diff --git a/pkg/goexec/tsch/module.go b/pkg/goexec/tsch/module.go index a02158b..72acf9b 100644 --- a/pkg/goexec/tsch/module.go +++ b/pkg/goexec/tsch/module.go @@ -63,7 +63,7 @@ func (m *Tsch) registerTask(ctx context.Context, opts *registerOptions, in *goex ctx = log.WithContext(ctx) - principalId := "1" // This value can be anything + principalId := "LocalSystem" settings := taskSettings{ MultipleInstancesPolicy: "IgnoreNew", @@ -101,7 +101,7 @@ func (m *Tsch) registerTask(ctx context.Context, opts *registerOptions, in *goex }, } - def := task{ + def := simpleTask{ TaskVersion: TaskXmlVersion, TaskNamespace: TaskXmlNamespace, Triggers: opts.triggers, diff --git a/pkg/goexec/tsch/task/action.go b/pkg/goexec/tsch/task/action.go new file mode 100644 index 0000000..de6c29f --- /dev/null +++ b/pkg/goexec/tsch/task/action.go @@ -0,0 +1,229 @@ +package task + +import ( + "encoding/xml" +) + +// --------------------------------------------------------------------------- +// shared base +// --------------------------------------------------------------------------- + +// ActionType is the base for all actions (only carries the optional id attribute). +type ActionType struct { + XMLName xml.Name `xml:"-"` + Id string `xml:"id,attr,omitempty"` +} + +// --------------------------------------------------------------------------- +// Exec +// --------------------------------------------------------------------------- + +// ExecAction corresponds to <Exec> (execActionType). +type ExecAction struct { + XMLName xml.Name `xml:"Exec"` + ActionType + + // <Command> is the program or script to run. + Command string `xml:"Command"` + // <Arguments> are passed to the Command. + Arguments string `xml:"Arguments,omitempty"` + // <WorkingDirectory> sets the cwd for the process. + WorkingDirectory string `xml:"WorkingDirectory,omitempty"` +} + +// --------------------------------------------------------------------------- +// ComHandler +// --------------------------------------------------------------------------- + +// ComHandlerAction corresponds to <ComHandler> (comHandlerActionType). +type ComHandlerAction struct { + XMLName xml.Name `xml:"ComHandler"` + ActionType + + // <ClassId> is the COM class ID (GUID). + ClassId string `xml:"ClassId"` + // <Data> is passed into the handler (optional). + Data string `xml:"Data,omitempty"` +} + +// --------------------------------------------------------------------------- +// SendEmail +// --------------------------------------------------------------------------- + +// SendEmailAction corresponds to <SendEmail> (sendEmailActionType). +type SendEmailAction struct { + XMLName xml.Name `xml:"SendEmail"` + ActionType + + Server string `xml:"Server"` // SMTP server + Subject string `xml:"Subject"` // email subject + To string `xml:"To"` // semicolon‑separated + Cc string `xml:"Cc,omitempty"` + Bcc string `xml:"Bcc,omitempty"` + ReplyTo string `xml:"ReplyTo,omitempty"` + Body string `xml:"Body,omitempty"` + // optional named header fields + HeaderFields *NamedValues `xml:"HeaderFields,omitempty"` +} + +// --------------------------------------------------------------------------- +// ShowMessage +// --------------------------------------------------------------------------- + +// ShowMessageAction corresponds to <ShowMessage> (showMessageActionType). +type ShowMessageAction struct { + XMLName xml.Name `xml:"ShowMessage"` + ActionType + + Title string `xml:"Title"` // window title + Message string `xml:"Message"` // body text +} + +// --------------------------------------------------------------------------- +// NamedValues (used by SendEmailAction.HeaderFields) +// --------------------------------------------------------------------------- + +// NamedValues holds zero or more <Value name="…">…</Value> entries. +type NamedValues struct { + XMLName xml.Name `xml:"HeaderFields"` + Value []NamedValue `xml:"Value"` +} + +// NamedValue is one name/value pair. +type NamedValue struct { + XMLName xml.Name `xml:"Value"` + Name string `xml:"name,attr"` + Value string `xml:",chardata"` +} + +// --------------------------------------------------------------------------- +// Actions container +// --------------------------------------------------------------------------- + +// Actions corresponds to <Actions> (actionsType). +// It may contain any number of each action type, in any order, +// and carries an optional Context attribute. +type Actions struct { + XMLName xml.Name `xml:"Actions"` + + // Context="" lets you override the default ("Author"). + Context string `xml:"Context,attr,omitempty"` + + Exec []ExecAction `xml:"Exec,omitempty"` + ComHandler []ComHandlerAction `xml:"ComHandler,omitempty"` + SendEmail []SendEmailAction `xml:"SendEmail,omitempty"` + ShowMessage []ShowMessageAction `xml:"ShowMessage,omitempty"` +} + +/* +// --------------------------------------------------------------------------- +// Marshal / Unmarshal helpers +// --------------------------------------------------------------------------- + +// MarshalXML satisfies xml.Marshaler. +// It writes out the <Actions> start tag (with optional Context attr), +// then each child action in declaration order, then the end tag. +func (a *Actions) MarshalXML(e *xml.Encoder, start xml.StartElement) error { + // prepare start element + start.Name.Local = "Actions" + if a.Context != "" { + start.Attr = append(start.Attr, + xml.Attr{Name: xml.Name{Local: "Context"}, Value: a.Context}, + ) + } + // write <Actions ...> + if err := e.EncodeToken(start); err != nil { + return err + } + // write children + for _, act := range a.Exec { + if err := e.Encode(act); err != nil { + return err + } + } + for _, act := range a.ComHandler { + if err := e.Encode(act); err != nil { + return err + } + } + for _, act := range a.SendEmail { + if err := e.Encode(act); err != nil { + return err + } + } + for _, act := range a.ShowMessage { + if err := e.Encode(act); err != nil { + return err + } + } + // write </Actions> + if err := e.EncodeToken(xml.EndElement{Name: start.Name}); err != nil { + return err + } + return e.Flush() +} + +// UnmarshalXML satisfies xml.Unmarshaler. +// It reads the <Actions> element (capturing Context attr), +// then loops decoding any Exec, ComHandler, SendEmail, or ShowMessage children. +func (a *Actions) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { + // capture Context attribute + for _, attr := range start.Attr { + if attr.Name.Local == "Context" { + a.Context = attr.Value + } + } + + // iterate tokens until </Actions> + for { + tok, err := d.Token() + if err != nil { + return err + } + switch t := tok.(type) { + case xml.StartElement: + switch t.Name.Local { + case "Exec": + var act ExecAction + if err := d.DecodeElement(&act, &t); err != nil { + return err + } + a.Exec = append(a.Exec, act) + + case "ComHandler": + var act ComHandlerAction + if err := d.DecodeElement(&act, &t); err != nil { + return err + } + a.ComHandler = append(a.ComHandler, act) + + case "SendEmail": + var act SendEmailAction + if err := d.DecodeElement(&act, &t); err != nil { + return err + } + a.SendEmail = append(a.SendEmail, act) + + case "ShowMessage": + var act ShowMessageAction + if err := d.DecodeElement(&act, &t); err != nil { + return err + } + a.ShowMessage = append(a.ShowMessage, act) + + default: + // skip any unknown elements + if err := d.Skip(); err != nil { + return err + } + } + + case xml.EndElement: + if t.Name.Local == start.Name.Local { + // finished + return nil + } + } + } +} +*/ diff --git a/pkg/goexec/tsch/task/misc.go b/pkg/goexec/tsch/task/misc.go new file mode 100644 index 0000000..fad6515 --- /dev/null +++ b/pkg/goexec/tsch/task/misc.go @@ -0,0 +1,90 @@ +package task + +import "encoding/xml" + +// --------------------------------------------------------------------------- +// RegistrationInfo (registrationInfoType) +// --------------------------------------------------------------------------- + +// NamedValuePair represents one <Identification name="…" value="…"/> +// entry within RegistrationInfo. +type NamedValuePair struct { + XMLName xml.Name `xml:"Identification"` + Name string `xml:"name,attr"` + Value string `xml:"value,attr"` +} + +// RegistrationInfo corresponds to the <RegistrationInfo> element. +// +// Fields are all optional and appear in the same order as in the XSD. +type RegistrationInfo struct { + XMLName xml.Name `xml:"RegistrationInfo"` + + Date string `xml:"Date,omitempty"` // xs:dateTime + Author string `xml:"Author,omitempty"` // xs:string + Description string `xml:"Description,omitempty"` // xs:string + URI string `xml:"URI,omitempty"` // xs:string + Version string `xml:"Version,omitempty"` // xs:string + Source string `xml:"Source,omitempty"` // xs:string + Documentation string `xml:"Documentation,omitempty"` // xs:string + SecurityDescriptor string `xml:"SecurityDescriptor,omitempty"` // xs:string (SDDL) + Identification []NamedValuePair `xml:"Identification,omitempty"` // zero or more +} + +// --------------------------------------------------------------------------- +// Data (dataType) +// --------------------------------------------------------------------------- + +// Data corresponds to the <Data> element under a TaskDefinition. +// It can contain any well‑formed XML inside. +type Data struct { + XMLName xml.Name `xml:"Data"` + InnerXML string `xml:",innerxml"` +} + +// --------------------------------------------------------------------------- +// Principal (principalType) +// --------------------------------------------------------------------------- + +// RunLevelType enumerates the RunLevel element values. +type RunLevelType string + +const ( + RunLevelLeastPrivilege RunLevelType = "LeastPrivilege" + RunLevelHighestAvailable RunLevelType = "HighestAvailable" +) + +// LogonType enumerates the LogonType element values. +type LogonType string + +const ( + LogonTypeNone LogonType = "None" + LogonTypePassword LogonType = "Password" + LogonTypeInteractiveToken LogonType = "InteractiveToken" + LogonTypeS4U LogonType = "S4U" + LogonTypeVirtualAccount LogonType = "VirtualAccount" + LogonTypeGroup LogonType = "Group" +) + +// --------------------------------------------------------------------------- +// Principals container (principalsType) +// --------------------------------------------------------------------------- + +// Principals corresponds to the <Principals> element. +// It holds one or more <Principal> entries. +type Principals struct { + XMLName xml.Name `xml:"Principals"` + Principal []Principal `xml:"Principal"` +} + +// Principal corresponds to the <Principal> element within <Principals>. +type Principal struct { + XMLName xml.Name `xml:"Principal"` + Id string `xml:"id,attr,omitempty"` + + UserId string `xml:"UserId,omitempty"` // xs:string + GroupId string `xml:"GroupId,omitempty"` // xs:string + RunLevel RunLevelType `xml:"RunLevel,omitempty"` // default="LeastPrivilege" + LogonType LogonType `xml:"LogonType,omitempty"` // default="InteractiveToken" + DisplayName string `xml:"DisplayName,omitempty"` // xs:string +} diff --git a/pkg/goexec/tsch/task/settings.go b/pkg/goexec/tsch/task/settings.go new file mode 100644 index 0000000..9dbe1e5 --- /dev/null +++ b/pkg/goexec/tsch/task/settings.go @@ -0,0 +1,65 @@ +package task + +import "encoding/xml" + +// Settings mirrors the <Settings> element (settingsType). +type Settings struct { + XMLName xml.Name `xml:"Settings"` + + AllowStartOnDemand bool `xml:"AllowStartOnDemand,omitempty"` + RestartOnFailure *RestartOnFailure `xml:"RestartOnFailure,omitempty"` + MultipleInstancesPolicy MultipleInstancesPolicy `xml:"MultipleInstancesPolicy,omitempty"` + DisallowStartIfOnBatteries bool `xml:"DisallowStartIfOnBatteries,omitempty"` + StopIfGoingOnBatteries bool `xml:"StopIfGoingOnBatteries,omitempty"` + AllowHardTerminate bool `xml:"AllowHardTerminate,omitempty"` + StartWhenAvailable bool `xml:"StartWhenAvailable,omitempty"` + NetworkProfileName string `xml:"NetworkProfileName,omitempty"` + RunOnlyIfNetworkAvailable bool `xml:"RunOnlyIfNetworkAvailable,omitempty"` + WakeToRun bool `xml:"WakeToRun,omitempty"` + Enabled bool `xml:"Enabled,omitempty"` + Hidden bool `xml:"Hidden,omitempty"` + DeleteExpiredTaskAfter string `xml:"DeleteExpiredTaskAfter,omitempty"` + IdleSettings *IdleSettings `xml:"IdleSettings,omitempty"` + NetworkSettings *NetworkSettings `xml:"NetworkSettings,omitempty"` + ExecutionTimeLimit string `xml:"ExecutionTimeLimit,omitempty"` + Priority byte `xml:"Priority,omitempty"` + RunOnlyIfIdle bool `xml:"RunOnlyIfIdle,omitempty"` + UseUnifiedSchedulingEngine bool `xml:"UseUnifiedSchedulingEngine,omitempty"` + DisallowStartOnRemoteAppSession bool `xml:"DisallowStartOnRemoteAppSession,omitempty"` +} + +// RestartOnFailure corresponds to <RestartOnFailure> (restartType), +// retrying a failed task. +type RestartOnFailure struct { + XMLName xml.Name `xml:"RestartOnFailure"` + Interval string `xml:"Interval"` // xs:duration (min PT1M, max P31D) + Count uint8 `xml:"Count"` // unsignedByte ≥1 +} + +// MultipleInstancesPolicy enumerates policies for concurrent task instances. +type MultipleInstancesPolicy string + +const ( + Parallel MultipleInstancesPolicy = "Parallel" + Queue MultipleInstancesPolicy = "Queue" + IgnoreNew MultipleInstancesPolicy = "IgnoreNew" + StopExisting MultipleInstancesPolicy = "StopExisting" +) + +// IdleSettings corresponds to <IdleSettings> (idleSettingsType), +// controlling idle‐based execution. +type IdleSettings struct { + XMLName xml.Name `xml:"IdleSettings"` + StopOnIdleEnd bool `xml:"StopOnIdleEnd,omitempty"` + RestartOnIdle bool `xml:"RestartOnIdle,omitempty"` + Duration string `xml:"Duration,omitempty"` // xs:duration (deprecated) + WaitTimeout string `xml:"WaitTimeout,omitempty"` // xs:duration (deprecated) +} + +// NetworkSettings corresponds to <NetworkSettings> (networkSettingsType), +// specifying which network profile to await. +type NetworkSettings struct { + XMLName xml.Name `xml:"NetworkSettings"` + Name string `xml:"Name,omitempty"` // nonEmptyString + Id string `xml:"Id,omitempty"` // guidType +} diff --git a/pkg/goexec/tsch/task/task.go b/pkg/goexec/tsch/task/task.go new file mode 100644 index 0000000..2d53e5b --- /dev/null +++ b/pkg/goexec/tsch/task/task.go @@ -0,0 +1,22 @@ +package task + +import "encoding/xml" + +// --------------------------------------------------------------------------- +// Task (TaskDefinitionType / <Task> root element) +// --------------------------------------------------------------------------- + +// Task represents the root <Task> element (type TaskDefinitionType). +// It pulls together RegistrationInfo, Triggers, Principals, Settings, Actions, and Data. +type Task struct { + XMLName xml.Name `xml:"Task"` + Version string `xml:"version,attr"` // required + Xmlns string `xml:"xmlns,attr,omitempty"` // e.g. "http://schemas.microsoft.com/windows/2004/02/mit/task" + + RegistrationInfo *RegistrationInfo `xml:"RegistrationInfo,omitempty"` + Triggers *Triggers `xml:"Triggers,omitempty"` + Principals *Principals `xml:"Principals,omitempty"` + Settings *Settings `xml:"Settings,omitempty"` + Actions *Actions `xml:"Actions"` // required + Data *Data `xml:"Data,omitempty"` +} diff --git a/pkg/goexec/tsch/task/trigger.go b/pkg/goexec/tsch/task/trigger.go new file mode 100644 index 0000000..695bf5b --- /dev/null +++ b/pkg/goexec/tsch/task/trigger.go @@ -0,0 +1,195 @@ +package task + +import "encoding/xml" + +// Triggers corresponds to the <Triggers> container (triggersType) +// and may hold any number of each trigger type, in schema order. +type Triggers struct { + XMLName xml.Name `xml:"Triggers"` + Boot []BootTrigger `xml:"BootTrigger,omitempty"` + Time []TimeTrigger `xml:"TimeTrigger,omitempty"` + Calendar []CalendarTrigger `xml:"CalendarTrigger,omitempty"` + Event []EventTrigger `xml:"EventTrigger,omitempty"` + Idle []IdleTrigger `xml:"IdleTrigger,omitempty"` + Logon []LogonTrigger `xml:"LogonTrigger,omitempty"` + Registration []RegistrationTrigger `xml:"RegistrationTrigger,omitempty"` + SessionStateChange []SessionStateChangeTrigger `xml:"SessionStateChangeTrigger,omitempty"` +} + +// Repetition corresponds to the <Repetition> element (repetitionType), +// defining how often and for how long a trigger will re‑fire. +type Repetition struct { + XMLName xml.Name `xml:"Repetition"` + Interval string `xml:"Interval"` // duration, e.g. PT5M + StopAtDurationEnd bool `xml:"StopAtDurationEnd,omitempty"` // default=false + Duration string `xml:"Duration,omitempty"` // duration, max span +} + +// BootTrigger starts a task when the system boots. +// Inherits StartBoundary, EndBoundary, Enabled, Repetition, ExecutionTimeLimit. +type BootTrigger struct { + XMLName xml.Name `xml:"BootTrigger"` + Id string `xml:"id,attr,omitempty"` + StartBoundary string `xml:"StartBoundary"` + EndBoundary string `xml:"EndBoundary,omitempty"` + Enabled bool `xml:"Enabled,omitempty"` + Repetition *Repetition `xml:"Repetition,omitempty"` + ExecutionTimeLimit string `xml:"ExecutionTimeLimit,omitempty"` + Delay string `xml:"Delay,omitempty"` // duration after boot +} + +// TimeTrigger fires once at a given time. +// Adds RandomDelay to the base trigger. +type TimeTrigger struct { + XMLName xml.Name `xml:"TimeTrigger"` + Id string `xml:"id,attr,omitempty"` + StartBoundary string `xml:"StartBoundary"` + EndBoundary string `xml:"EndBoundary,omitempty"` + Enabled bool `xml:"Enabled,omitempty"` + Repetition *Repetition `xml:"Repetition,omitempty"` + ExecutionTimeLimit string `xml:"ExecutionTimeLimit,omitempty"` + RandomDelay string `xml:"RandomDelay,omitempty"` // optional jitter +} + +// CalendarTrigger covers daily, weekly, monthly & DOW schedules. +type CalendarTrigger struct { + XMLName xml.Name `xml:"CalendarTrigger"` + Id string `xml:"id,attr,omitempty"` + StartBoundary string `xml:"StartBoundary"` + EndBoundary string `xml:"EndBoundary,omitempty"` + Enabled bool `xml:"Enabled,omitempty"` + Repetition *Repetition `xml:"Repetition,omitempty"` + ExecutionTimeLimit string `xml:"ExecutionTimeLimit,omitempty"` + RandomDelay string `xml:"RandomDelay,omitempty"` + ScheduleByDay *DailySchedule `xml:"ScheduleByDay,omitempty"` + ScheduleByWeek *WeeklySchedule `xml:"ScheduleByWeek,omitempty"` + ScheduleByMonth *MonthlySchedule `xml:"ScheduleByMonth,omitempty"` + ScheduleByMonthDayOfWeek *MonthlyDOWSchedule `xml:"ScheduleByMonthDayOfWeek,omitempty"` +} + +// Support types for CalendarTrigger: + +// DailySchedule (dailyScheduleType): interval in days. +type DailySchedule struct { + DaysInterval int `xml:"DaysInterval,omitempty"` +} + +// WeeklySchedule (weeklyScheduleType): weeks interval + days flag. +type WeeklySchedule struct { + WeeksInterval int `xml:"WeeksInterval,omitempty"` + DaysOfWeek *DaysOfWeek `xml:"DaysOfWeek,omitempty"` +} + +// MonthlySchedule (monthlyScheduleType): specific month days + months. +type MonthlySchedule struct { + DaysOfMonth *DaysOfMonth `xml:"DaysOfMonth,omitempty"` + Months *Months `xml:"Months,omitempty"` +} + +// MonthlyDOWSchedule (monthlyDayOfWeekScheduleType): weeks of month + days + months. +type MonthlyDOWSchedule struct { + Weeks *Weeks `xml:"Weeks,omitempty"` + DaysOfWeek DaysOfWeek `xml:"DaysOfWeek"` + Months *Months `xml:"Months,omitempty"` +} + +// DaysOfWeek (daysOfWeekType): a set of empty elements indicating which weekdays. +type DaysOfWeek struct { + Monday *struct{} `xml:"Monday,omitempty"` + Tuesday *struct{} `xml:"Tuesday,omitempty"` + Wednesday *struct{} `xml:"Wednesday,omitempty"` + Thursday *struct{} `xml:"Thursday,omitempty"` + Friday *struct{} `xml:"Friday,omitempty"` + Saturday *struct{} `xml:"Saturday,omitempty"` + Sunday *struct{} `xml:"Sunday,omitempty"` +} + +// DaysOfMonth (daysOfMonthType): list of numeric days in a month. +type DaysOfMonth struct { + Day []int `xml:"Day,omitempty"` +} + +// Months (monthsType): empty elements for each month. +type Months struct { + January *struct{} `xml:"January,omitempty"` + February *struct{} `xml:"February,omitempty"` + March *struct{} `xml:"March,omitempty"` + April *struct{} `xml:"April,omitempty"` + May *struct{} `xml:"May,omitempty"` + June *struct{} `xml:"June,omitempty"` + July *struct{} `xml:"July,omitempty"` + August *struct{} `xml:"August,omitempty"` + September *struct{} `xml:"September,omitempty"` + October *struct{} `xml:"October,omitempty"` + November *struct{} `xml:"November,omitempty"` + December *struct{} `xml:"December,omitempty"` +} + +// Weeks (weeksType): list of "1"–"4" or "Last". +type Weeks struct { + Week []string `xml:"Week,omitempty"` +} + +// EventTrigger fires on matching Windows events. +type EventTrigger struct { + XMLName xml.Name `xml:"EventTrigger"` + Id string `xml:"id,attr,omitempty"` + StartBoundary string `xml:"StartBoundary,omitempty"` + EndBoundary string `xml:"EndBoundary,omitempty"` + Enabled bool `xml:"Enabled,omitempty"` + Repetition *Repetition `xml:"Repetition,omitempty"` + ExecutionTimeLimit string `xml:"ExecutionTimeLimit,omitempty"` + Subscription string `xml:"Subscription"` // XPath query + Delay string `xml:"Delay,omitempty"` + ValueQueries *NamedValues `xml:"ValueQueries,omitempty"` +} + +// IdleTrigger fires when the system goes idle. +type IdleTrigger struct { + XMLName xml.Name `xml:"IdleTrigger"` + Id string `xml:"id,attr,omitempty"` + StartBoundary string `xml:"StartBoundary"` + EndBoundary string `xml:"EndBoundary,omitempty"` + Enabled bool `xml:"Enabled,omitempty"` + Repetition *Repetition `xml:"Repetition,omitempty"` + ExecutionTimeLimit string `xml:"ExecutionTimeLimit,omitempty"` +} + +// LogonTrigger fires on user logon (optionally scoped by UserId). +type LogonTrigger struct { + XMLName xml.Name `xml:"LogonTrigger"` + Id string `xml:"id,attr,omitempty"` + StartBoundary string `xml:"StartBoundary"` + EndBoundary string `xml:"EndBoundary,omitempty"` + Enabled bool `xml:"Enabled,omitempty"` + Repetition *Repetition `xml:"Repetition,omitempty"` + ExecutionTimeLimit string `xml:"ExecutionTimeLimit,omitempty"` + UserId string `xml:"UserId,omitempty"` + Delay string `xml:"Delay,omitempty"` +} + +// RegistrationTrigger fires when the task is registered or updated. +type RegistrationTrigger struct { + XMLName xml.Name `xml:"RegistrationTrigger"` + Id string `xml:"id,attr,omitempty"` + StartBoundary string `xml:"StartBoundary"` + EndBoundary string `xml:"EndBoundary,omitempty"` + Enabled bool `xml:"Enabled,omitempty"` + Repetition *Repetition `xml:"Repetition,omitempty"` + ExecutionTimeLimit string `xml:"ExecutionTimeLimit,omitempty"` + Delay string `xml:"Delay,omitempty"` +} + +// SessionStateChangeTrigger fires on terminal‑server session changes. +type SessionStateChangeTrigger struct { + XMLName xml.Name `xml:"SessionStateChangeTrigger"` + Id string `xml:"id,attr,omitempty"` + StartBoundary string `xml:"StartBoundary,omitempty"` + EndBoundary string `xml:"EndBoundary,omitempty"` + Enabled bool `xml:"Enabled,omitempty"` + Repetition *Repetition `xml:"Repetition,omitempty"` + ExecutionTimeLimit string `xml:"ExecutionTimeLimit,omitempty"` + StateChange string `xml:"StateChange"` // e.g. “Connect” or “Disconnect” + UserId string `xml:"UserId,omitempty"` + Delay string `xml:"Delay,omitempty"` +} diff --git a/pkg/goexec/tsch/tsch.go b/pkg/goexec/tsch/tsch.go index e51433d..ae65ca7 100644 --- a/pkg/goexec/tsch/tsch.go +++ b/pkg/goexec/tsch/tsch.go @@ -82,7 +82,7 @@ type taskPrincipal struct { RunLevel string `xml:"RunLevel"` } -type task struct { +type simpleTask struct { XMLName xml.Name `xml:"Task"` TaskVersion string `xml:"version,attr"` TaskNamespace string `xml:"xmlns,attr"` @@ -110,7 +110,7 @@ func newSettings(terminate, onDemand, startWhenAvailable bool) *taskSettings { } // newTask creates a task with any static values filled -func newTask(se *taskSettings, pr []taskPrincipal, tr taskTriggers, cmd, args string) *task { +func newTask(se *taskSettings, pr []taskPrincipal, tr taskTriggers, cmd, args string) *simpleTask { if se == nil { se = newSettings(true, true, false) } @@ -123,7 +123,7 @@ func newTask(se *taskSettings, pr []taskPrincipal, tr taskTriggers, cmd, args st }, } } - return &task{ + return &simpleTask{ TaskVersion: "1.2", TaskNamespace: "http://schemas.microsoft.com/windows/2004/02/mit/task", Triggers: tr, |