aboutsummaryrefslogtreecommitdiff
path: root/pkg
diff options
context:
space:
mode:
authorBryan McNulty <bryanmcnulty@protonmail.com>2025-04-20 11:26:44 -0500
committerBryan McNulty <bryanmcnulty@protonmail.com>2025-04-20 11:26:44 -0500
commitce79cf929133ea2592fb899d6339c1e299aa9eeb (patch)
tree964763333f623969febd5f1ef86a00862a83b591 /pkg
parent61578457eed9243d3be1bb120cce5995e149adec (diff)
downloadgoexec-ce79cf929133ea2592fb899d6339c1e299aa9eeb.tar.gz
goexec-ce79cf929133ea2592fb899d6339c1e299aa9eeb.zip
Added `tsch change` command
Diffstat (limited to 'pkg')
-rw-r--r--pkg/goexec/tsch/change.go152
-rw-r--r--pkg/goexec/tsch/demand.go4
-rw-r--r--pkg/goexec/tsch/module.go4
-rw-r--r--pkg/goexec/tsch/task/action.go229
-rw-r--r--pkg/goexec/tsch/task/misc.go90
-rw-r--r--pkg/goexec/tsch/task/settings.go65
-rw-r--r--pkg/goexec/tsch/task/task.go22
-rw-r--r--pkg/goexec/tsch/task/trigger.go195
-rw-r--r--pkg/goexec/tsch/tsch.go6
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,