diff --git a/README.md b/README.md index 653fe42..a1f8652 100644 --- a/README.md +++ b/README.md @@ -3,12 +3,12 @@ https://github.com/amidaware/tacticalrmm #### building the agent - linux ``` -env CGO_ENABLED=0 GOOS= GOARCH= go build -ldflags "-s -w" +env CGO_ENABLED=0 GOOS= GOARCH= go build -ldflags "-s -w -X 'main.Version=v2.0.4" ``` #### building the agent - windows ``` -$env:CGO_ENABLED="0";$env:GOOS="windows";$env:GOARCH="amd64"; go build -ldflags "-s -w" +$env:CGO_ENABLED="0";$env:GOOS="windows";$env:GOARCH="amd64"; go build -ldflags "-s -w -X 'main.Version=v2.0.4" ``` ### tests diff --git a/agent/choco/choco.go b/agent/choco/choco.go new file mode 100644 index 0000000..92c48ce --- /dev/null +++ b/agent/choco/choco.go @@ -0,0 +1,62 @@ +package choco + +import ( + "time" + + "github.com/amidaware/rmmagent/agent/system" + "github.com/amidaware/rmmagent/agent/tactical/api" + "github.com/amidaware/rmmagent/agent/tactical/config" + "github.com/go-resty/resty/v2" +) + +func InstallChoco() { + config := config.NewAgentConfig() + var result ChocoInstalled + result.AgentID = config.AgentID + result.Installed = false + + rClient := resty.New() + rClient.SetTimeout(30 * time.Second) + if len(config.Proxy) > 0 { + rClient.SetProxy(config.Proxy) + } + + url := "/api/v3/choco/" + r, err := rClient.R().Get("https://chocolatey.org/install.ps1") + if err != nil { + api.PostPayload(result, url) + return + } + + if r.IsError() { + api.PostPayload(result, url) + return + } + + _, _, exitcode, err := system.RunScript(string(r.Body()), "powershell", []string{}, 900) + if err != nil { + api.PostPayload(result, url) + return + } + + if exitcode != 0 { + api.PostPayload(result, url) + return + } + + result.Installed = true + api.PostPayload(result, url) +} + +func InstallWithChoco(name string) (string, error) { + out, err := system.CMD("choco.exe", []string{"install", name, "--yes", "--force", "--force-dependencies", "--no-progress"}, 1200, false) + if err != nil { + return err.Error(), err + } + + if out[1] != "" { + return out[1], nil + } + + return out[0], nil +} diff --git a/agent/choco/structs.go b/agent/choco/structs.go new file mode 100644 index 0000000..250e6c8 --- /dev/null +++ b/agent/choco/structs.go @@ -0,0 +1,6 @@ +package choco + +type ChocoInstalled struct { + AgentID string `json:"agent_id"` + Installed bool `json:"installed"` +} diff --git a/agent/network/network.go b/agent/network/network.go new file mode 100644 index 0000000..98df898 --- /dev/null +++ b/agent/network/network.go @@ -0,0 +1,57 @@ +package network + +import ( + "net" + "time" + + "github.com/amidaware/rmmagent/agent/utils" + "github.com/go-resty/resty/v2" +) + +// PublicIP returns the agent's public ip +// Tries 3 times before giving up +func PublicIP(proxy string) string { + client := resty.New() + client.SetTimeout(4 * time.Second) + if len(proxy) > 0 { + client.SetProxy(proxy) + } + + urls := []string{"https://icanhazip.tacticalrmm.io/", "https://icanhazip.com", "https://ifconfig.co/ip"} + ip := "error" + for _, url := range urls { + r, err := client.R().Get(url) + if err != nil { + continue + } + + ip = utils.StripAll(r.String()) + if !IsValidIP(ip) { + continue + } + + v4 := net.ParseIP(ip) + if v4.To4() == nil { + r1, err := client.R().Get("https://ifconfig.me/ip") + if err != nil { + return ip + } + + ipv4 := utils.StripAll(r1.String()) + if !IsValidIP(ipv4) { + continue + } + + return ipv4 + } + + break + } + + return ip +} + +// IsValidIP checks for a valid ipv4 or ipv6 +func IsValidIP(ip string) bool { + return net.ParseIP(ip) != nil +} diff --git a/agent/patching/patching_windows.go b/agent/patching/patching_windows.go index d9885e3..92bc5ef 100644 --- a/agent/patching/patching_windows.go +++ b/agent/patching/patching_windows.go @@ -1,7 +1,13 @@ package patching import ( + "fmt" + "time" + "github.com/amidaware/rmmagent/agent/patching/wua" + "github.com/amidaware/rmmagent/agent/system" + "github.com/amidaware/rmmagent/agent/tactical/api" + "github.com/amidaware/rmmagent/agent/tactical/config" "golang.org/x/sys/windows/registry" ) @@ -59,3 +65,68 @@ func GetUpdates() (PackageList, error) { // a.Logger.Debugln(err) // } } + +func InstallUpdates(guids []string) { + config := config.NewAgentConfig() + session, err := wua.NewUpdateSession() + if err != nil { + return + } + + defer session.Close() + for _, id := range guids { + var result WinUpdateInstallResult + result.AgentID = config.AgentID + result.UpdateID = id + query := fmt.Sprintf("UpdateID='%s'", id) + updts, err := session.GetWUAUpdateCollection(query) + if err != nil { + result.Success = false + api.Patch(result, "/api/v3/winupdates/") + continue + } + + defer updts.Release() + updtCnt, err := updts.Count() + if err != nil { + result.Success = false + api.Patch(result, "/api/v3/winupdates/") + continue + } + + if updtCnt == 0 { + superseded := SupersededUpdate{AgentID: config.AgentID, UpdateID: id} + api.PostPayload(superseded, "/api/v3/superseded/") + continue + } + + for i := 0; i < int(updtCnt); i++ { + u, err := updts.Item(i) + if err != nil { + result.Success = false + api.Patch(result, "/api/v3/winupdates/") + continue + } + + err = session.InstallWUAUpdate(u) + if err != nil { + result.Success = false + api.Patch(result, "/api/v3/winupdates/") + continue + } + + result.Success = true + api.Patch(result, "/api/v3/winupdates/") + } + } + + time.Sleep(5 * time.Second) + needsReboot, err := system.SystemRebootRequired() + if err != nil { + } + + rebootPayload := AgentNeedsReboot{AgentID: config.AgentID, NeedsReboot: needsReboot} + err = api.Put(rebootPayload, "/api/v3/winupdates/") + if err != nil { + } +} diff --git a/agent/patching/structs.go b/agent/patching/structs.go index dbc1a93..1dfe7f4 100644 --- a/agent/patching/structs.go +++ b/agent/patching/structs.go @@ -14,3 +14,19 @@ type Package struct { Installed bool `json:"installed"` Downloaded bool `json:"downloaded"` } + +type WinUpdateInstallResult struct { + AgentID string `json:"agent_id"` + UpdateID string `json:"guid"` + Success bool `json:"success"` +} + +type SupersededUpdate struct { + AgentID string `json:"agent_id"` + UpdateID string `json:"guid"` +} + +type AgentNeedsReboot struct { + AgentID string `json:"agent_id"` + NeedsReboot bool `json:"needs_reboot"` +} diff --git a/agent/services/services_windows.go b/agent/services/services_windows.go index 0ae8a70..7c774ef 100644 --- a/agent/services/services_windows.go +++ b/agent/services/services_windows.go @@ -2,9 +2,11 @@ package services import ( "fmt" + "time" "github.com/amidaware/rmmagent/agent/utils" "github.com/gonutz/w32/v2" + "golang.org/x/sys/windows/svc" "golang.org/x/sys/windows/svc/mgr" ) @@ -162,3 +164,136 @@ func serviceStartType(num uint32) string { return "Unknown" } } + +func ControlService(name, action string) WinSvcResp { + conn, err := mgr.Connect() + if err != nil { + return WinSvcResp{Success: false, ErrorMsg: err.Error()} + } + + defer conn.Disconnect() + srv, err := conn.OpenService(name) + if err != nil { + return WinSvcResp{Success: false, ErrorMsg: err.Error()} + } + + defer srv.Close() + var status svc.Status + switch action { + + case "stop": + status, err = srv.Control(svc.Stop) + if err != nil { + return WinSvcResp{Success: false, ErrorMsg: err.Error()} + } + timeout := time.Now().Add(30 * time.Second) + for status.State != svc.Stopped { + if timeout.Before(time.Now()) { + return WinSvcResp{Success: false, ErrorMsg: "Timed out waiting for service to stop"} + } + + time.Sleep(500 * time.Millisecond) + status, err = srv.Query() + if err != nil { + return WinSvcResp{Success: false, ErrorMsg: err.Error()} + } + } + + return WinSvcResp{Success: true, ErrorMsg: ""} + + case "start": + err := srv.Start() + if err != nil { + return WinSvcResp{Success: false, ErrorMsg: err.Error()} + } + + return WinSvcResp{Success: true, ErrorMsg: ""} + } + + return WinSvcResp{Success: false, ErrorMsg: "Something went wrong"} +} + +func GetServiceDetail(name string) Service { + ret := Service{} + + conn, err := mgr.Connect() + if err != nil { + return ret + } + + defer conn.Disconnect() + srv, err := conn.OpenService(name) + if err != nil { + return ret + } + + defer srv.Close() + q, err := srv.Query() + if err != nil { + return ret + } + + conf, err := srv.Config() + if err != nil { + return ret + } + + ret.BinPath = utils.CleanString(conf.BinaryPathName) + ret.Description = utils.CleanString(conf.Description) + ret.DisplayName = utils.CleanString(conf.DisplayName) + ret.Name = name + ret.PID = q.ProcessId + ret.StartType = serviceStartType(uint32(conf.StartType)) + ret.Status = serviceStatusText(uint32(q.State)) + ret.Username = utils.CleanString(conf.ServiceStartName) + ret.DelayedAutoStart = conf.DelayedAutoStart + return ret +} + +func EditService(name, startupType string) WinSvcResp { + conn, err := mgr.Connect() + if err != nil { + return WinSvcResp{Success: false, ErrorMsg: err.Error()} + } + defer conn.Disconnect() + + srv, err := conn.OpenService(name) + if err != nil { + return WinSvcResp{Success: false, ErrorMsg: err.Error()} + } + defer srv.Close() + + conf, err := srv.Config() + if err != nil { + return WinSvcResp{Success: false, ErrorMsg: err.Error()} + } + + var startType uint32 + switch startupType { + case "auto": + startType = 2 + case "autodelay": + startType = 2 + case "manual": + startType = 3 + case "disabled": + startType = 4 + default: + return WinSvcResp{Success: false, ErrorMsg: "Unknown startup type provided"} + } + + conf.StartType = startType + switch startupType { + case "autodelay": + conf.DelayedAutoStart = true + case "auto": + conf.DelayedAutoStart = false + } + + err = srv.UpdateConfig(conf) + if err != nil { + return WinSvcResp{Success: false, ErrorMsg: err.Error()} + } + + return WinSvcResp{Success: true, ErrorMsg: ""} +} diff --git a/agent/services/structs.go b/agent/services/structs.go index db758c9..614eaf8 100644 --- a/agent/services/structs.go +++ b/agent/services/structs.go @@ -11,3 +11,8 @@ type Service struct { StartType string `json:"start_type"` DelayedAutoStart bool `json:"autodelay"` } + +type WinSvcResp struct { + Success bool `json:"success"` + ErrorMsg string `json:"errormsg"` +} diff --git a/agent/system/structs.go b/agent/system/structs.go index 60b4e71..a32eb49 100644 --- a/agent/system/structs.go +++ b/agent/system/structs.go @@ -12,4 +12,13 @@ type CmdOptions struct { Detached bool } -type SchedTask struct{ Name string } \ No newline at end of file +type SchedTask struct{ Name string } + +type ProcessMsg struct { + Name string `json:"name"` + Pid int `json:"pid"` + MemBytes uint64 `json:"membytes"` + Username string `json:"username"` + UID int `json:"id"` + CPU string `json:"cpu_percent"` +} diff --git a/agent/system/system.go b/agent/system/system.go index 0c3b94c..9eafc85 100644 --- a/agent/system/system.go +++ b/agent/system/system.go @@ -9,11 +9,14 @@ import ( "math" "os" "os/exec" + "strconv" "time" "github.com/amidaware/rmmagent/agent/utils" ps "github.com/elastic/go-sysinfo" gocmd "github.com/go-cmd/cmd" + "github.com/shirou/gopsutil/cpu" + gops "github.com/shirou/gopsutil/v3/process" ) type CmdStatus struct { @@ -196,3 +199,68 @@ func BootTime() int64 { info := host.Info() return info.BootTime.Unix() } + +func GetCPULoadAvg() int { + fallback := false + pyCode := ` +import psutil +try: + print(int(round(psutil.cpu_percent(interval=10))), end='') +except: + print("pyerror", end='') +` + pypercent, err := RunPythonCode(pyCode, 13, []string{}) + if err != nil || pypercent == "pyerror" { + fallback = true + } + + i, err := strconv.Atoi(pypercent) + if err != nil { + fallback = true + } + + if fallback { + percent, err := cpu.Percent(10*time.Second, false) + if err != nil { + return 0 + } + + return int(math.Round(percent[0])) + } + + return i +} + +func GetProcsRPC() []ProcessMsg { + ret := make([]ProcessMsg, 0) + + procs, _ := ps.Processes() + for i, process := range procs { + p, err := process.Info() + if err != nil { + continue + } + if p.PID == 0 { + continue + } + + m, _ := process.Memory() + proc, gerr := gops.NewProcess(int32(p.PID)) + if gerr != nil { + continue + } + cpu, _ := proc.CPUPercent() + user, _ := proc.Username() + + ret = append(ret, ProcessMsg{ + Name: p.Name, + Pid: p.PID, + MemBytes: m.Resident, + Username: user, + UID: i, + CPU: fmt.Sprintf("%.1f", cpu), + }) + } + + return ret +} diff --git a/agent/tactical/api/api.go b/agent/tactical/api/api.go index 909be89..bed3496 100644 --- a/agent/tactical/api/api.go +++ b/agent/tactical/api/api.go @@ -37,11 +37,7 @@ func init() { func PostPayload(payload interface{}, url string) error { _, err := restyC.R().SetBody(payload).Post("/api/v3/syncmesh/") - if err != nil { - return err - } - - return nil + return err } func GetResult(result interface{}, url string) (*resty.Response, error) { @@ -52,3 +48,22 @@ func GetResult(result interface{}, url string) (*resty.Response, error) { return r, nil } + +func Get(url string) (*resty.Response, error) { + r, err := restyC.R().Get(url) + if err != nil { + return nil, err + } + + return r, nil +} + +func Patch(payload interface{}, url string) error { + _, err := restyC.R().SetBody(payload).Patch(url) + return err +} + +func Put(payload interface{}, url string) error { + _, err := restyC.R().SetBody(payload).Put(url) + return err +} diff --git a/agent/tactical/checks/checks.go b/agent/tactical/checks/checks.go new file mode 100644 index 0000000..e0dc313 --- /dev/null +++ b/agent/tactical/checks/checks.go @@ -0,0 +1,421 @@ +package checks + +import ( + "bytes" + "encoding/json" + "fmt" + "math" + "strings" + "sync" + "time" + + "github.com/amidaware/rmmagent/agent/events" + "github.com/amidaware/rmmagent/agent/services" + "github.com/amidaware/rmmagent/agent/system" + "github.com/amidaware/rmmagent/agent/tactical/api" + "github.com/amidaware/rmmagent/agent/utils" + ps "github.com/elastic/go-sysinfo" + "github.com/go-ping/ping" + "github.com/shirou/gopsutil/disk" +) + +func CheckRunner(agentID string) error { + sleepDelay := utils.RandRange(14, 22) + time.Sleep(time.Duration(sleepDelay) * time.Second) + for { + interval, err := GetCheckInterval(agentID) + if err == nil && !ChecksRunning() { + _, err = system.CMD(system.GetProgramEXE(), []string{"-m", "checkrunner"}, 600, false) + if err != nil { + return err + } + } + + time.Sleep(time.Duration(interval) * time.Second) + } +} + +func GetCheckInterval(agentID string) (int, error) { + r, err := api.GetResult(CheckInfo{}, fmt.Sprintf("/api/v3/%s/checkinterval/", agentID)) + if err != nil { + return 120, err + } + + if r.IsError() { + return 120, fmt.Errorf("checkinterval response code: %v", r.StatusCode()) + } + + interval := r.Result().(*CheckInfo).Interval + return interval, nil +} + +// ChecksRunning prevents duplicate checks from running +// Have to do it this way, can't use atomic because they can run from both rpc and tacticalagent services +func ChecksRunning() bool { + running := false + procs, err := ps.Processes() + if err != nil { + return running + } +Out: + for _, process := range procs { + p, err := process.Info() + if err != nil { + continue + } + if p.PID == 0 { + continue + } + if p.Exe != system.GetProgramEXE() { + continue + } + + for _, arg := range p.Args { + if arg == "runchecks" || arg == "checkrunner" { + running = true + break Out + } + } + } + + return running +} + +func RunChecks(agentID string, force bool) error { + data := AllChecks{} + var url string + if force { + url = fmt.Sprintf("/api/v3/%s/runchecks/", agentID) + } else { + url = fmt.Sprintf("/api/v3/%s/checkrunner/", agentID) + } + + r, err := api.Get(url) + if err != nil { + return err + } + + if r.IsError() { + return fmt.Errorf("response error: %d", r.StatusCode()) + } + + if err := json.Unmarshal(r.Body(), &data); err != nil { + return err + } + + var wg sync.WaitGroup + eventLogChecks := make([]Check, 0) + winServiceChecks := make([]Check, 0) + for _, check := range data.Checks { + switch check.CheckType { + case "diskspace": + wg.Add(1) + go func(c Check, wg *sync.WaitGroup) { + defer wg.Done() + utils.RandomCheckDelay() + SendDiskCheckResult(DiskCheck(c)) + }(check, &wg) + case "cpuload": + wg.Add(1) + go func(c Check, wg *sync.WaitGroup) { + defer wg.Done() + CPULoadCheck(c) + }(check, &wg) + case "memory": + wg.Add(1) + go func(c Check, wg *sync.WaitGroup) { + defer wg.Done() + utils.RandomCheckDelay() + MemCheck(c) + }(check, &wg) + case "ping": + wg.Add(1) + go func(c Check, wg *sync.WaitGroup) { + defer wg.Done() + utils.RandomCheckDelay() + SendPingCheckResult(PingCheck(c)) + }(check, &wg) + case "script": + wg.Add(1) + go func(c Check, wg *sync.WaitGroup) { + defer wg.Done() + utils.RandomCheckDelay() + ScriptCheck(c) + }(check, &wg) + case "winsvc": + winServiceChecks = append(winServiceChecks, check) + case "eventlog": + eventLogChecks = append(eventLogChecks, check) + default: + continue + } + } + + if len(winServiceChecks) > 0 { + wg.Add(len(winServiceChecks)) + go func(wg *sync.WaitGroup) { + for _, winSvcCheck := range winServiceChecks { + defer wg.Done() + SendWinSvcCheckResult(WinSvcCheck(winSvcCheck)) + } + }(&wg) + } + + if len(eventLogChecks) > 0 { + wg.Add(len(eventLogChecks)) + go func(wg *sync.WaitGroup) { + for _, evtCheck := range eventLogChecks { + defer wg.Done() + EventLogCheck(evtCheck) + } + }(&wg) + } + + wg.Wait() + return nil +} + +func SendDiskCheckResult(payload DiskCheckResult) error { + err := api.Patch(payload, "/api/v3/checkrunner/") + if err != nil { + return err + } + + return nil +} + +func DiskCheck(data Check) (payload DiskCheckResult) { + payload.ID = data.CheckPK + usage, err := disk.Usage(data.Disk) + if err != nil { + payload.Exists = false + payload.MoreInfo = fmt.Sprintf("Disk %s does not exist", data.Disk) + } + + payload.Exists = true + payload.PercentUsed = usage.UsedPercent + payload.MoreInfo = fmt.Sprintf("Total: %s, Free: %s", utils.ByteCountSI(usage.Total), utils.ByteCountSI(usage.Free)) + + return +} + +func CPULoadCheck(data Check) error { + payload := CPUMemResult{ + ID: data.CheckPK, + Percent: system.GetCPULoadAvg(), + } + + err := api.PostPayload(payload, "/api/v3/checkrunner/") + if err != nil { + return err + } + + return nil +} + +func MemCheck(data Check) error { + host, _ := ps.Host() + mem, _ := host.Memory() + percent := (float64(mem.Used) / float64(mem.Total)) * 100 + + payload := CPUMemResult{ID: data.CheckPK, Percent: int(math.Round(percent))} + err := api.PostPayload(payload, "/api/v3/checkrunner/") + if err != nil { + return err + } + + return nil +} + +func SendPingCheckResult(payload PingCheckResponse) error { + err := api.Patch(payload, "/api/v3/checkrunner/") + if err != nil { + return err + } + + return nil +} + +func PingCheck(data Check) (payload PingCheckResponse) { + payload.ID = data.CheckPK + + out, err := DoPing(data.IP) + if err != nil { + payload.Status = "failing" + payload.Output = err.Error() + return + } + + payload.Status = out.Status + payload.Output = out.Output + return +} + +func DoPing(host string) (PingResponse, error) { + var ret PingResponse + pinger, err := ping.NewPinger(host) + if err != nil { + return ret, err + } + + var buf bytes.Buffer + pinger.OnRecv = func(pkt *ping.Packet) { + fmt.Fprintf(&buf, "%d bytes from %s: icmp_seq=%d time=%v\n", + pkt.Nbytes, pkt.IPAddr, pkt.Seq, pkt.Rtt) + } + + pinger.OnFinish = func(stats *ping.Statistics) { + fmt.Fprintf(&buf, "\n--- %s ping statistics ---\n", stats.Addr) + fmt.Fprintf(&buf, "%d packets transmitted, %d packets received, %v%% packet loss\n", + stats.PacketsSent, stats.PacketsRecv, stats.PacketLoss) + fmt.Fprintf(&buf, "round-trip min/avg/max/stddev = %v/%v/%v/%v\n", + stats.MinRtt, stats.AvgRtt, stats.MaxRtt, stats.StdDevRtt) + } + + pinger.Count = 3 + pinger.Size = 24 + pinger.Interval = time.Second + pinger.Timeout = 5 * time.Second + pinger.SetPrivileged(true) + + err = pinger.Run() + if err != nil { + return ret, err + } + + ret.Output = buf.String() + + stats := pinger.Statistics() + + if stats.PacketsRecv == stats.PacketsSent || stats.PacketLoss == 0 { + ret.Status = "passing" + } else { + ret.Status = "failing" + } + + return ret, nil +} + +func ScriptCheck(data Check) error { + start := time.Now() + stdout, stderr, retcode, _ := system.RunScript(data.Script.Code, data.Script.Shell, data.ScriptArgs, data.Timeout) + + payload := ScriptCheckResult{ + ID: data.CheckPK, + Stdout: stdout, + Stderr: stderr, + Retcode: retcode, + Runtime: time.Since(start).Seconds(), + } + + err := api.Patch(payload, "/api/v3/checkrunner/") + if err != nil { + return err + } + + return nil +} + +func SendWinSvcCheckResult(payload WinSvcCheckResult) error { + err := api.Patch(payload, "/api/v3/checkrunner/") + if err != nil { + return err + } + + return nil +} + +func WinSvcCheck(data Check) (payload WinSvcCheckResult) { + payload.ID = data.CheckPK + status, err := services.GetServiceStatus(data.ServiceName) + if err != nil { + if data.PassNotExist { + payload.Status = "passing" + } else { + payload.Status = "failing" + } + + payload.MoreInfo = err.Error() + return + } + + payload.MoreInfo = fmt.Sprintf("Status: %s", status) + if status == "running" { + payload.Status = "passing" + } else if status == "start_pending" && data.PassStartPending { + payload.Status = "passing" + } else { + if data.RestartIfStopped { + ret := services.ControlService(data.ServiceName, "start") + if ret.Success { + payload.Status = "passing" + payload.MoreInfo = "Status: running" + } else { + payload.Status = "failing" + } + } else { + payload.Status = "failing" + } + } + return +} + +func EventLogCheck(data Check) error { + log := make([]events.EventLogMsg, 0) + evtLog, _ := events.GetEventLog(data.LogName, data.SearchLastDays) + + for _, i := range evtLog { + if i.EventType == data.EventType { + if !data.EventIDWildcard && !(int(i.EventID) == data.EventID) { + continue + } + + if data.EventSource == "" && data.EventMessage == "" { + if data.EventIDWildcard { + log = append(log, i) + } else if int(i.EventID) == data.EventID { + log = append(log, i) + } else { + continue + } + } + + if data.EventSource != "" && data.EventMessage != "" { + if data.EventIDWildcard { + if strings.Contains(i.Source, data.EventSource) && strings.Contains(i.Message, data.EventMessage) { + log = append(log, i) + } + } else if int(i.EventID) == data.EventID { + if strings.Contains(i.Source, data.EventSource) && strings.Contains(i.Message, data.EventMessage) { + log = append(log, i) + } + } + continue + } + + if data.EventSource != "" && strings.Contains(i.Source, data.EventSource) { + if data.EventIDWildcard { + log = append(log, i) + } else if int(i.EventID) == data.EventID { + log = append(log, i) + } + } + + if data.EventMessage != "" && strings.Contains(i.Message, data.EventMessage) { + if data.EventIDWildcard { + log = append(log, i) + } else if int(i.EventID) == data.EventID { + log = append(log, i) + } + } + } + } + + payload := EventLogCheckResult{ID: data.CheckPK, Log: log} + err := api.Patch(payload, "/api/v3/checkrunner/") + if err != nil { + return err + } + + return nil +} diff --git a/agent/tactical/checks/checks_windows.go b/agent/tactical/checks/checks_windows.go deleted file mode 100644 index 734513b..0000000 --- a/agent/tactical/checks/checks_windows.go +++ /dev/null @@ -1,175 +0,0 @@ -package checks - -import ( - "encoding/json" - "fmt" - "sync" - "time" - - "github.com/amidaware/rmmagent/agent/system" - "github.com/amidaware/rmmagent/agent/tactical/api" - "github.com/amidaware/rmmagent/agent/utils" - ps "github.com/elastic/go-sysinfo" - "github.com/go-resty/resty/v2" -) - -func CheckRunner(agentID string) error { - sleepDelay := utils.RandRange(14, 22) - time.Sleep(time.Duration(sleepDelay) * time.Second) - for { - interval, err := GetCheckInterval(agentID) - if err == nil && !ChecksRunning() { - _, err = system.CMD(system.GetProgramEXE(), []string{"-m", "checkrunner"}, 600, false) - if err != nil { - return err - } - } - - time.Sleep(time.Duration(interval) * time.Second) - } - - return nil -} - -func GetCheckInterval(agentID string) (int, error) { - r, err := api.GetResult(CheckInfo{}, fmt.Sprintf("/api/v3/%s/checkinterval/", agentID)) - if err != nil { - return 120, err - } - - if r.IsError() { - return 120, fmt.Errorf("checkinterval response code: %v", r.StatusCode()) - } - - interval := r.Result().(*CheckInfo).Interval - return interval, nil -} - -// ChecksRunning prevents duplicate checks from running -// Have to do it this way, can't use atomic because they can run from both rpc and tacticalagent services -func ChecksRunning() bool { - running := false - procs, err := ps.Processes() - if err != nil { - return running - } -Out: - for _, process := range procs { - p, err := process.Info() - if err != nil { - continue - } - if p.PID == 0 { - continue - } - if p.Exe != system.GetProgramEXE() { - continue - } - - for _, arg := range p.Args { - if arg == "runchecks" || arg == "checkrunner" { - running = true - break Out - } - } - } - - return running -} - -func RunChecks(agentID string, force bool) error { - data := rmm.AllChecks{} - var url string - if force { - url = fmt.Sprintf("/api/v3/%s/runchecks/", agentID) - } else { - url = fmt.Sprintf("/api/v3/%s/checkrunner/", agentID) - } - - r, err := a.rClient.R().Get(url) - if err != nil { - //a.Logger.Debugln(err) - return err - } - - if r.IsError() { - //a.Logger.Debugln("Checkrunner response code:", r.StatusCode()) - return nil - } - - if err := json.Unmarshal(r.Body(), &data); err != nil { - //a.Logger.Debugln(err) - return err - } - - var wg sync.WaitGroup - eventLogChecks := make([]rmm.Check, 0) - winServiceChecks := make([]rmm.Check, 0) - for _, check := range data.Checks { - switch check.CheckType { - case "diskspace": - wg.Add(1) - go func(c rmm.Check, wg *sync.WaitGroup, r *resty.Client) { - defer wg.Done() - utils.RandomCheckDelay() - a.SendDiskCheckResult(a.DiskCheck(c), r) - }(check, &wg, a.rClient) - case "cpuload": - wg.Add(1) - go func(c rmm.Check, wg *sync.WaitGroup, r *resty.Client) { - defer wg.Done() - a.CPULoadCheck(c, r) - }(check, &wg, a.rClient) - case "memory": - wg.Add(1) - go func(c rmm.Check, wg *sync.WaitGroup, r *resty.Client) { - defer wg.Done() - randomCheckDelay() - a.MemCheck(c, r) - }(check, &wg, a.rClient) - case "ping": - wg.Add(1) - go func(c rmm.Check, wg *sync.WaitGroup, r *resty.Client) { - defer wg.Done() - randomCheckDelay() - a.SendPingCheckResult(a.PingCheck(c), r) - }(check, &wg, a.rClient) - case "script": - wg.Add(1) - go func(c rmm.Check, wg *sync.WaitGroup, r *resty.Client) { - defer wg.Done() - randomCheckDelay() - a.ScriptCheck(c, r) - }(check, &wg, a.rClient) - case "winsvc": - winServiceChecks = append(winServiceChecks, check) - case "eventlog": - eventLogChecks = append(eventLogChecks, check) - default: - continue - } - } - - if len(winServiceChecks) > 0 { - wg.Add(len(winServiceChecks)) - go func(wg *sync.WaitGroup, r *resty.Client) { - for _, winSvcCheck := range winServiceChecks { - defer wg.Done() - a.SendWinSvcCheckResult(a.WinSvcCheck(winSvcCheck), r) - } - }(&wg, a.rClient) - } - - if len(eventLogChecks) > 0 { - wg.Add(len(eventLogChecks)) - go func(wg *sync.WaitGroup, r *resty.Client) { - for _, evtCheck := range eventLogChecks { - defer wg.Done() - a.EventLogCheck(evtCheck, r) - } - }(&wg, a.rClient) - } - - wg.Wait() - return nil -} diff --git a/agent/tactical/checks/structs.go b/agent/tactical/checks/structs.go index 47e560f..a171d13 100644 --- a/agent/tactical/checks/structs.go +++ b/agent/tactical/checks/structs.go @@ -1,6 +1,91 @@ package checks +import "github.com/amidaware/rmmagent/agent/events" + type CheckInfo struct { AgentPK int `json:"agent"` Interval int `json:"check_interval"` -} \ No newline at end of file +} + +type AllChecks struct { + CheckInfo + Checks []Check +} + +type AssignedTask struct { + TaskPK int `json:"id"` + Enabled bool `json:"enabled"` +} + +type Script struct { + Shell string `json:"shell"` + Code string `json:"code"` +} + +type Check struct { + Script Script `json:"script"` + AssignedTasks []AssignedTask `json:"assigned_tasks"` + CheckPK int `json:"id"` + CheckType string `json:"check_type"` + Status string `json:"status"` + Threshold int `json:"threshold"` + Disk string `json:"disk"` + IP string `json:"ip"` + ScriptArgs []string `json:"script_args"` + Timeout int `json:"timeout"` + ServiceName string `json:"svc_name"` + PassStartPending bool `json:"pass_if_start_pending"` + PassNotExist bool `json:"pass_if_svc_not_exist"` + RestartIfStopped bool `json:"restart_if_stopped"` + LogName string `json:"log_name"` + EventID int `json:"event_id"` + EventIDWildcard bool `json:"event_id_is_wildcard"` + EventType string `json:"event_type"` + EventSource string `json:"event_source"` + EventMessage string `json:"event_message"` + FailWhen string `json:"fail_when"` + SearchLastDays int `json:"search_last_days"` +} + +type DiskCheckResult struct { + ID int `json:"id"` + MoreInfo string `json:"more_info"` + PercentUsed float64 `json:"percent_used"` + Exists bool `json:"exists"` +} + +type CPUMemResult struct { + ID int `json:"id"` + Percent int `json:"percent"` +} + +type PingCheckResponse struct { + ID int `json:"id"` + AgentID string `json:"agent_id"` + Status string `json:"status"` + Output string `json:"output"` +} + +type PingResponse struct { + Status string + Output string +} + +type ScriptCheckResult struct { + ID int `json:"id"` + Stdout string `json:"stdout"` + Stderr string `json:"stderr"` + Retcode int `json:"retcode"` + Runtime float64 `json:"runtime"` +} + +type WinSvcCheckResult struct { + ID int `json:"id"` + MoreInfo string `json:"more_info"` + Status string `json:"status"` +} + +type EventLogCheckResult struct { + ID int `json:"id"` + Log []events.EventLogMsg `json:"log"` +} diff --git a/agent/tactical/mesh/mesh.go b/agent/tactical/mesh/mesh.go index e3e8a60..b0c702f 100644 --- a/agent/tactical/mesh/mesh.go +++ b/agent/tactical/mesh/mesh.go @@ -2,10 +2,12 @@ package mesh import ( "github.com/amidaware/rmmagent/agent/tactical/api" + "github.com/amidaware/rmmagent/agent/tactical/config" "github.com/amidaware/rmmagent/agent/utils" ) -func SyncMeshNodeID(agentID string) error { +func SyncMeshNodeID() error { + config := config.NewAgentConfig() id, err := GetMeshNodeID() if err != nil { return err @@ -13,7 +15,7 @@ func SyncMeshNodeID(agentID string) error { payload := MeshNodeID{ Func: "syncmesh", - Agentid: agentID, + Agentid: config.AgentID, NodeID: utils.StripAll(id), } diff --git a/agent/tactical/mesh/mesh_windows.go b/agent/tactical/mesh/mesh_windows.go index dd8dca9..ff7ee45 100644 --- a/agent/tactical/mesh/mesh_windows.go +++ b/agent/tactical/mesh/mesh_windows.go @@ -2,9 +2,11 @@ package mesh import ( "errors" + "fmt" "os" "path/filepath" "strings" + "time" "github.com/amidaware/rmmagent/agent/system" "github.com/amidaware/rmmagent/agent/tactical/config" @@ -71,7 +73,7 @@ func getMeshBinLocation() string { return MeshSysBin } -func installMesh(meshbin, exe, proxy string) (string, error) { +func InstallMesh(meshbin, exe, proxy string) (string, error) { var meshNodeID string meshInstallArgs := []string{"-fullinstall"} if len(proxy) > 0 { @@ -79,7 +81,6 @@ func installMesh(meshbin, exe, proxy string) (string, error) { meshInstallArgs = append(meshInstallArgs, meshProxy) } - //a.Logger.Debugln("Mesh install args:", meshInstallArgs) meshOut, meshErr := system.CMD(meshbin, meshInstallArgs, int(90), false) if meshErr != nil { fmt.Println(meshOut[0]) @@ -88,30 +89,24 @@ func installMesh(meshbin, exe, proxy string) (string, error) { } fmt.Println(meshOut) - //a.Logger.Debugln("Sleeping for 5") time.Sleep(5 * time.Second) meshSuccess := false for !meshSuccess { - //a.Logger.Debugln("Getting mesh node id") pMesh, pErr := system.CMD(exe, []string{"-nodeid"}, int(30), false) if pErr != nil { - //a.Logger.Errorln(pErr) time.Sleep(5 * time.Second) continue } if pMesh[1] != "" { - //a.Logger.Errorln(pMesh[1]) time.Sleep(5 * time.Second) continue } meshNodeID = utils.StripAll(pMesh[0]) - //a.Logger.Debugln("Node id:", meshNodeID) if strings.Contains(strings.ToLower(meshNodeID), "not defined") { - //a.Logger.Errorln(meshNodeID) time.Sleep(5 * time.Second) continue } @@ -121,3 +116,10 @@ func installMesh(meshbin, exe, proxy string) (string, error) { return meshNodeID, nil } + +func RecoverMesh() { + defer system.CMD("net", []string{"start", "mesh agent"}, 60, false) + _, _ = system.CMD("net", []string{"stop", "mesh agent"}, 60, false) + ForceKillMesh() + SyncMeshNodeID() +} diff --git a/agent/tactical/rpc/rpc_linux.go b/agent/tactical/rpc/rpc_linux.go new file mode 100644 index 0000000..98f0ea1 --- /dev/null +++ b/agent/tactical/rpc/rpc_linux.go @@ -0,0 +1,4 @@ +package rpc + +func RunRPC(version string) { +} diff --git a/agent/tactical/rpc/rpc_windows.go b/agent/tactical/rpc/rpc_windows.go index 5e85b07..9636176 100644 --- a/agent/tactical/rpc/rpc_windows.go +++ b/agent/tactical/rpc/rpc_windows.go @@ -9,35 +9,51 @@ import ( "sync/atomic" "time" + "github.com/amidaware/rmmagent/agent/choco" "github.com/amidaware/rmmagent/agent/events" + "github.com/amidaware/rmmagent/agent/network" "github.com/amidaware/rmmagent/agent/patching" + "github.com/amidaware/rmmagent/agent/services" + "github.com/amidaware/rmmagent/agent/software" + "github.com/amidaware/rmmagent/agent/system" + "github.com/amidaware/rmmagent/agent/tactical" + "github.com/amidaware/rmmagent/agent/tactical/api" + "github.com/amidaware/rmmagent/agent/tactical/checks" + "github.com/amidaware/rmmagent/agent/tactical/config" + "github.com/amidaware/rmmagent/agent/tactical/mesh" "github.com/amidaware/rmmagent/agent/tactical/service" + "github.com/amidaware/rmmagent/agent/tactical/shared" + ttasks "github.com/amidaware/rmmagent/agent/tactical/tasks" "github.com/amidaware/rmmagent/agent/tasks" - rmm "github.com/amidaware/rmmagent/shared" + ksvc "github.com/kardianos/service" nats "github.com/nats-io/nats.go" "github.com/ugorji/go/codec" ) -func RunRPC(a *rmm.AgentConfig) { - //a.Logger.Infoln("Agent service started") - go service.RunAsService() +var ( + agentUpdateLocker uint32 + getWinUpdateLocker uint32 + installWinUpdateLocker uint32 +) + +func RunRPC(version string) { + config := config.NewAgentConfig() + go service.RunAsService(version) var wg sync.WaitGroup wg.Add(1) opts := service.SetupNatsOptions() - server := fmt.Sprintf("tls://%s:4222", a.APIURL) + server := fmt.Sprintf("tls://%s:4222", config.APIURL) nc, err := nats.Connect(server, opts...) if err != nil { - //a.Logger.Fatalln("RunRPC() nats.Connect()", err) } - nc.Subscribe(a.AgentID, func(msg *nats.Msg) { + nc.Subscribe(config.AgentID, func(msg *nats.Msg) { var payload *NatsMsg var mh codec.MsgpackHandle mh.RawToString = true dec := codec.NewDecoderBytes(msg.Data, &mh) if err := dec.Decode(&payload); err != nil { - //a.Logger.Errorln(err) return } @@ -46,7 +62,6 @@ func RunRPC(a *rmm.AgentConfig) { go func() { var resp []byte ret := codec.NewEncoderBytes(&resp, new(codec.MsgpackHandle)) - //a.Logger.Debugln("pong") ret.Encode("pong") msg.Respond(resp) }() @@ -57,7 +72,6 @@ func RunRPC(a *rmm.AgentConfig) { ret := codec.NewEncoderBytes(&resp, new(codec.MsgpackHandle)) err := patching.PatchMgmnt(p.PatchMgmt) if err != nil { - //a.Logger.Errorln("PatchMgmnt:", err.Error()) ret.Encode(err.Error()) } else { ret.Encode("ok") @@ -71,7 +85,6 @@ func RunRPC(a *rmm.AgentConfig) { ret := codec.NewEncoderBytes(&resp, new(codec.MsgpackHandle)) success, err := tasks.CreateSchedTask(p.ScheduledTask) if err != nil { - //a.Logger.Errorln(err.Error()) ret.Encode(err.Error()) } else if !success { ret.Encode("Something went wrong") @@ -87,7 +100,6 @@ func RunRPC(a *rmm.AgentConfig) { ret := codec.NewEncoderBytes(&resp, new(codec.MsgpackHandle)) err := tasks.DeleteSchedTask(p.ScheduledTask.Name) if err != nil { - //a.Logger.Errorln(err.Error()) ret.Encode(err.Error()) } else { ret.Encode("ok") @@ -99,8 +111,7 @@ func RunRPC(a *rmm.AgentConfig) { go func() { var resp []byte ret := codec.NewEncoderBytes(&resp, new(codec.MsgpackHandle)) - tasks := tasks.ListSchedTasks() - //a.Logger.Debugln(tasks) + tasks, _ := tasks.ListSchedTasks() ret.Encode(tasks) msg.Respond(resp) }() @@ -110,8 +121,7 @@ func RunRPC(a *rmm.AgentConfig) { var resp []byte ret := codec.NewEncoderBytes(&resp, new(codec.MsgpackHandle)) days, _ := strconv.Atoi(p.Data["days"]) - evtLog := events.GetEventLog(p.Data["logname"], days) - //a.Logger.Debugln(evtLog) + evtLog, _ := events.GetEventLog(p.Data["logname"], days) ret.Encode(evtLog) msg.Respond(resp) }(payload) @@ -120,7 +130,7 @@ func RunRPC(a *rmm.AgentConfig) { go func() { var resp []byte ret := codec.NewEncoderBytes(&resp, new(codec.MsgpackHandle)) - procs := a.GetProcsRPC() + procs := system.GetProcsRPC() //a.Logger.Debugln(procs) ret.Encode(procs) msg.Respond(resp) @@ -130,10 +140,9 @@ func RunRPC(a *rmm.AgentConfig) { go func(p *NatsMsg) { var resp []byte ret := codec.NewEncoderBytes(&resp, new(codec.MsgpackHandle)) - err := KillProc(p.ProcPID) + err := system.KillProc(p.ProcPID) if err != nil { ret.Encode(err.Error()) - //a.Logger.Debugln(err.Error()) } else { ret.Encode("ok") } @@ -143,13 +152,12 @@ func RunRPC(a *rmm.AgentConfig) { case "rawcmd": go func(p *NatsMsg) { var resp []byte - var resultData rmm.RawCMDResp + var resultData RawCMDResp ret := codec.NewEncoderBytes(&resp, new(codec.MsgpackHandle)) switch runtime.GOOS { case "windows": - out, _ := CMDShell(p.Data["shell"], []string{}, p.Data["command"], p.Timeout, false) - //a.Logger.Debugln(out) + out, _ := system.CMDShell(p.Data["shell"], []string{}, p.Data["command"], p.Timeout, false) if out[1] != "" { ret.Encode(out[1]) resultData.Results = out[1] @@ -158,11 +166,11 @@ func RunRPC(a *rmm.AgentConfig) { resultData.Results = out[0] } default: - opts := a.NewCMDOpts() + opts := system.NewCMDOpts() opts.Shell = p.Data["shell"] opts.Command = p.Data["command"] opts.Timeout = time.Duration(p.Timeout) - out := a.CmdV2(opts) + out := system.CmdV2(opts) tmp := "" if len(out.Stdout) > 0 { tmp += out.Stdout @@ -177,7 +185,7 @@ func RunRPC(a *rmm.AgentConfig) { msg.Respond(resp) if p.ID != 0 { - a.rClient.R().SetBody(resultData).Patch(fmt.Sprintf("/api/v3/%d/%s/histresult/", p.ID, a.AgentID)) + api.Patch(resultData, fmt.Sprintf("/api/v3/%d/%s/histresult/", p.ID, config.AgentID)) } }(payload) @@ -185,8 +193,7 @@ func RunRPC(a *rmm.AgentConfig) { go func() { var resp []byte ret := codec.NewEncoderBytes(&resp, new(codec.MsgpackHandle)) - svcs := a.GetServices() - //a.Logger.Debugln(svcs) + svcs, _, _ := services.GetServices() ret.Encode(svcs) msg.Respond(resp) }() @@ -195,8 +202,7 @@ func RunRPC(a *rmm.AgentConfig) { go func(p *NatsMsg) { var resp []byte ret := codec.NewEncoderBytes(&resp, new(codec.MsgpackHandle)) - svc := a.GetServiceDetail(p.Data["name"]) - //a.Logger.Debugln(svc) + svc := services.GetServiceDetail(p.Data["name"]) ret.Encode(svc) msg.Respond(resp) }(payload) @@ -205,8 +211,7 @@ func RunRPC(a *rmm.AgentConfig) { go func(p *NatsMsg) { var resp []byte ret := codec.NewEncoderBytes(&resp, new(codec.MsgpackHandle)) - retData := a.ControlService(p.Data["name"], p.Data["action"]) - //a.Logger.Debugln(retData) + retData := services.ControlService(p.Data["name"], p.Data["action"]) ret.Encode(retData) msg.Respond(resp) }(payload) @@ -215,8 +220,7 @@ func RunRPC(a *rmm.AgentConfig) { go func(p *NatsMsg) { var resp []byte ret := codec.NewEncoderBytes(&resp, new(codec.MsgpackHandle)) - retData := a.EditService(p.Data["name"], p.Data["startType"]) - //a.Logger.Debugln(retData) + retData := services.EditService(p.Data["name"], p.Data["startType"]) ret.Encode(retData) msg.Respond(resp) }(payload) @@ -225,15 +229,14 @@ func RunRPC(a *rmm.AgentConfig) { go func(p *NatsMsg) { var resp []byte var retData string - var resultData rmm.RunScriptResp + var resultData RunScriptResp ret := codec.NewEncoderBytes(&resp, new(codec.MsgpackHandle)) start := time.Now() - stdout, stderr, retcode, err := a.RunScript(p.Data["code"], p.Data["shell"], p.ScriptArgs, p.Timeout) + stdout, stderr, retcode, err := system.RunScript(p.Data["code"], p.Data["shell"], p.ScriptArgs, p.Timeout) resultData.ExecTime = time.Since(start).Seconds() resultData.ID = p.ID if err != nil { - //a.Logger.Debugln(err) retData = err.Error() resultData.Retcode = 1 resultData.Stderr = err.Error() @@ -248,17 +251,17 @@ func RunRPC(a *rmm.AgentConfig) { msg.Respond(resp) if p.ID != 0 { results := map[string]interface{}{"script_results": resultData} - a.rClient.R().SetBody(results).Patch(fmt.Sprintf("/api/v3/%d/%s/histresult/", p.ID, a.AgentID)) + api.Patch(results, fmt.Sprintf("/api/v3/%d/%s/histresult/", p.ID, config.AgentID)) } }(payload) case "runscriptfull": go func(p *NatsMsg) { var resp []byte - var retData rmm.RunScriptResp + var retData RunScriptResp ret := codec.NewEncoderBytes(&resp, new(codec.MsgpackHandle)) start := time.Now() - stdout, stderr, retcode, err := a.RunScript(p.Data["code"], p.Data["shell"], p.ScriptArgs, p.Timeout) + stdout, stderr, retcode, err := system.RunScript(p.Data["code"], p.Data["shell"], p.ScriptArgs, p.Timeout) retData.ExecTime = time.Since(start).Seconds() if err != nil { @@ -275,7 +278,7 @@ func RunRPC(a *rmm.AgentConfig) { msg.Respond(resp) if p.ID != 0 { results := map[string]interface{}{"script_results": retData} - a.rClient.R().SetBody(results).Patch(fmt.Sprintf("/api/v3/%d/%s/histresult/", p.ID, a.AgentID)) + api.Patch(results, fmt.Sprintf("/api/v3/%d/%s/histresult/", p.ID, config.AgentID)) } }(payload) @@ -286,8 +289,7 @@ func RunRPC(a *rmm.AgentConfig) { switch p.Data["mode"] { case "mesh": - //a.Logger.Debugln("Recovering mesh") - a.RecoverMesh() + mesh.RecoverMesh() } ret.Encode("ok") @@ -297,8 +299,7 @@ func RunRPC(a *rmm.AgentConfig) { go func() { var resp []byte ret := codec.NewEncoderBytes(&resp, new(codec.MsgpackHandle)) - sw := a.GetInstalledSoftware() - //a.Logger.Debugln(sw) + sw, _ := software.GetInstalledSoftware() ret.Encode(sw) msg.Respond(resp) }() @@ -311,24 +312,21 @@ func RunRPC(a *rmm.AgentConfig) { ret.Encode("ok") msg.Respond(resp) if runtime.GOOS == "windows" { - CMD("shutdown.exe", []string{"/r", "/t", "5", "/f"}, 15, false) + system.CMD("shutdown.exe", []string{"/r", "/t", "5", "/f"}, 15, false) } else { - opts := a.NewCMDOpts() + opts := system.NewCMDOpts() opts.Command = "reboot" - a.CmdV2(opts) + system.CmdV2(opts) } }() case "needsreboot": go func() { - //a.Logger.Debugln("Checking if reboot needed") var resp []byte ret := codec.NewEncoderBytes(&resp, new(codec.MsgpackHandle)) - out, err := a.SystemRebootRequired() + out, err := system.SystemRebootRequired() if err == nil { - //a.Logger.Debugln("Reboot needed:", out) ret.Encode(out) } else { - //a.Logger.Debugln("Error checking if reboot needed:", err) ret.Encode(false) } msg.Respond(resp) @@ -337,26 +335,22 @@ func RunRPC(a *rmm.AgentConfig) { go func() { var resp []byte ret := codec.NewEncoderBytes(&resp, new(codec.MsgpackHandle)) - //a.Logger.Debugln("Getting sysinfo with WMI") modes := []string{"agent-agentinfo", "agent-disks", "agent-wmi", "agent-publicip"} for _, m := range modes { - a.NatsMessage(nc, m) + service.NatsMessage(version, nc, m) } ret.Encode("ok") msg.Respond(resp) }() case "wmi": go func() { - //a.Logger.Debugln("Sending WMI") - a.NatsMessage(nc, "agent-wmi") + service.NatsMessage(version, nc, "agent-wmi") }() case "cpuloadavg": go func() { var resp []byte ret := codec.NewEncoderBytes(&resp, new(codec.MsgpackHandle)) - //a.Logger.Debugln("Getting CPU Load Avg") - loadAvg := a.GetCPULoadAvg() - //a.Logger.Debugln("CPU Load Avg:", loadAvg) + loadAvg := system.GetCPULoadAvg() ret.Encode(loadAvg) msg.Respond(resp) }() @@ -365,54 +359,49 @@ func RunRPC(a *rmm.AgentConfig) { var resp []byte ret := codec.NewEncoderBytes(&resp, new(codec.MsgpackHandle)) if runtime.GOOS == "windows" { - if a.ChecksRunning() { + if checks.ChecksRunning() { ret.Encode("busy") msg.Respond(resp) - //a.Logger.Debugln("Checks are already running, please wait") } else { ret.Encode("ok") msg.Respond(resp) - //a.Logger.Debugln("Running checks") - _, checkerr := CMD(a.EXE, []string{"-m", "runchecks"}, 600, false) + _, checkerr := system.CMD(system.GetProgramEXE(), []string{"-m", "runchecks"}, 600, false) if checkerr != nil { - //a.Logger.Errorln("RPC RunChecks", checkerr) } } } else { ret.Encode("ok") msg.Respond(resp) - //a.Logger.Debugln("Running checks") - a.RunChecks(true) + checks.RunChecks(config.AgentID, true) } }() case "runtask": go func(p *NatsMsg) { - //a.Logger.Debugln("Running task") - a.RunTask(p.TaskPK) + ttasks.RunTask(p.TaskPK) }(payload) case "publicip": go func() { var resp []byte ret := codec.NewEncoderBytes(&resp, new(codec.MsgpackHandle)) - ret.Encode(a.PublicIP()) + ret.Encode(network.PublicIP(config.Proxy)) msg.Respond(resp) }() case "installpython": - go a.GetPython(true) + go shared.GetPython(true) case "installchoco": - go a.InstallChoco() + go choco.InstallChoco() case "installwithchoco": go func(p *NatsMsg) { var resp []byte ret := codec.NewEncoderBytes(&resp, new(codec.MsgpackHandle)) ret.Encode("ok") msg.Respond(resp) - out, _ := a.InstallWithChoco(p.ChocoProgName) + out, _ := choco.InstallWithChoco(p.ChocoProgName) results := map[string]string{"results": out} url := fmt.Sprintf("/api/v3/%d/chocoresult/", p.PendingActionPK) - a.rClient.R().SetBody(results).Patch(url) + api.Patch(results, url) }(payload) case "getwinupdates": go func() { @@ -421,7 +410,7 @@ func RunRPC(a *rmm.AgentConfig) { } else { //a.Logger.Debugln("Checking for windows updates") defer atomic.StoreUint32(&getWinUpdateLocker, 0) - a.GetWinUpdates() + patching.GetUpdates() } }() case "installwinupdates": @@ -431,7 +420,7 @@ func RunRPC(a *rmm.AgentConfig) { } else { //a.Logger.Debugln("Installing windows updates", p.UpdateGUIDs) defer atomic.StoreUint32(&installWinUpdateLocker, 0) - a.InstallUpdates(p.UpdateGUIDs) + patching.InstallUpdates(p.UpdateGUIDs) } }(payload) case "agentupdate": @@ -445,7 +434,7 @@ func RunRPC(a *rmm.AgentConfig) { } else { ret.Encode("ok") msg.Respond(resp) - a.AgentUpdate(p.Data["url"], p.Data["inno"], p.Data["version"]) + tactical.AgentUpdate(p.Data["url"], p.Data["inno"], p.Data["version"]) atomic.StoreUint32(&agentUpdateLocker, 0) nc.Flush() nc.Close() @@ -459,7 +448,7 @@ func RunRPC(a *rmm.AgentConfig) { ret := codec.NewEncoderBytes(&resp, new(codec.MsgpackHandle)) ret.Encode("ok") msg.Respond(resp) - a.AgentUninstall(p.Code) + tactical.AgentUninstall(p.Code) nc.Flush() nc.Close() os.Exit(0) @@ -475,3 +464,8 @@ func RunRPC(a *rmm.AgentConfig) { wg.Wait() } + +func Start(version string, _ ksvc.Service) error { + go RunRPC(version) + return nil +} diff --git a/agent/tactical/rpc/structs.go b/agent/tactical/rpc/structs.go index d3247f0..596d047 100644 --- a/agent/tactical/rpc/structs.go +++ b/agent/tactical/rpc/structs.go @@ -17,4 +17,16 @@ type NatsMsg struct { PatchMgmt bool `json:"patch_mgmt"` ID int `json:"id"` Code string `json:"code"` -} \ No newline at end of file +} + +type RawCMDResp struct { + Results string `json:"results"` +} + +type RunScriptResp struct { + Stdout string `json:"stdout"` + Stderr string `json:"stderr"` + Retcode int `json:"retcode"` + ExecTime float64 `json:"execution_time"` + ID int `json:"id"` +} diff --git a/agent/tactical/service/service.go b/agent/tactical/service/service.go index 5cdbd6c..84bc27d 100644 --- a/agent/tactical/service/service.go +++ b/agent/tactical/service/service.go @@ -7,53 +7,53 @@ import ( "time" "github.com/amidaware/rmmagent/agent/disk" + "github.com/amidaware/rmmagent/agent/network" "github.com/amidaware/rmmagent/agent/services" "github.com/amidaware/rmmagent/agent/system" + "github.com/amidaware/rmmagent/agent/tactical/api" "github.com/amidaware/rmmagent/agent/tactical/checks" + "github.com/amidaware/rmmagent/agent/tactical/config" + "github.com/amidaware/rmmagent/agent/tactical/mesh" + "github.com/amidaware/rmmagent/agent/tactical/shared" "github.com/amidaware/rmmagent/agent/utils" "github.com/amidaware/rmmagent/agent/wmi" "github.com/nats-io/nats.go" "github.com/ugorji/go/codec" - trmm "github.com/wh1te909/trmm-shared" ) var natsCheckin = []string{"agent-hello", "agent-agentinfo", "agent-disks", "agent-winsvc", "agent-publicip", "agent-wmi"} -func RunAsService(agentID string, version string) { +func RunAsService(version string) { var wg sync.WaitGroup wg.Add(1) go AgentSvc(version) - go checks.CheckRunner(agentID) + go checks.CheckRunner(version) wg.Wait() } func AgentSvc(version string) { - config := tactical.NewAgentConfig() - go tactical.GetPython(false) + config := config.NewAgentConfig() + go shared.GetPython(false) utils.CreateTRMMTempDir() - tactical.RunMigrations() - + shared.RunMigrations() sleepDelay := utils.RandRange(14, 22) - //a.Logger.Debugf("AgentSvc() sleeping for %v seconds", sleepDelay) time.Sleep(time.Duration(sleepDelay) * time.Second) - - opts := SetupNatsOptions(config.AgentID, config.Token) + opts := SetupNatsOptions() server := fmt.Sprintf("tls://%s:4222", config.APIURL) nc, err := nats.Connect(server, opts...) if err != nil { - //a.Logger.Fatalln("AgentSvc() nats.Connect()", err) } for _, s := range natsCheckin { - NatsMessage(config.AgentID, version, nc, s) + NatsMessage(version, nc, s) time.Sleep(time.Duration(utils.RandRange(100, 400)) * time.Millisecond) } - go tactical.SyncMeshNodeID() + go mesh.SyncMeshNodeID() time.Sleep(time.Duration(utils.RandRange(1, 3)) * time.Second) AgentStartup(config.AgentID) - tactical.SendSoftware() + shared.SendSoftware() checkInHelloTicker := time.NewTicker(time.Duration(utils.RandRange(30, 60)) * time.Second) checkInAgentInfoTicker := time.NewTicker(time.Duration(utils.RandRange(200, 400)) * time.Second) @@ -67,29 +67,30 @@ func AgentSvc(version string) { for { select { case <-checkInHelloTicker.C: - NatsMessage(config.AgentID, version, nc, "agent-hello") + NatsMessage(version, nc, "agent-hello") case <-checkInAgentInfoTicker.C: - NatsMessage(config.AgentID, version, nc, "agent-agentinfo") + NatsMessage(version, nc, "agent-agentinfo") case <-checkInWinSvcTicker.C: - NatsMessage(config.AgentID, version, nc, "agent-winsvc") + NatsMessage(version, nc, "agent-winsvc") case <-checkInPubIPTicker.C: - NatsMessage(config.AgentID, version, nc, "agent-publicip") + NatsMessage(version, nc, "agent-publicip") case <-checkInDisksTicker.C: - NatsMessage(config.AgentID, version, nc, "agent-disks") + NatsMessage(version, nc, "agent-disks") case <-checkInSWTicker.C: - tactical.SendSoftware() + shared.SendSoftware() case <-checkInWMITicker.C: - NatsMessage(config.AgentID, version, nc, "agent-wmi") + NatsMessage(version, nc, "agent-wmi") case <-syncMeshTicker.C: - tactical.SyncMeshNodeID() + mesh.SyncMeshNodeID() } } } -func SetupNatsOptions(agentID string, token string) []nats.Option { +func SetupNatsOptions() []nats.Option { + config := config.NewAgentConfig() opts := make([]nats.Option, 0) opts = append(opts, nats.Name("TacticalRMM")) - opts = append(opts, nats.UserInfo(agentID, token)) + opts = append(opts, nats.UserInfo(config.AgentID, config.Token)) opts = append(opts, nats.ReconnectWait(time.Second*5)) opts = append(opts, nats.RetryOnFailedConnect(true)) opts = append(opts, nats.MaxReconnects(-1)) @@ -97,21 +98,23 @@ func SetupNatsOptions(agentID string, token string) []nats.Option { return opts } -func NatsMessage(agentID string, version string, nc *nats.Conn, mode string) { +func NatsMessage(version string, nc *nats.Conn, mode string) { + config := config.NewAgentConfig() var resp []byte var payload interface{} ret := codec.NewEncoderBytes(&resp, new(codec.MsgpackHandle)) switch mode { case "agent-hello": - payload = trmm.CheckInNats{ - Agentid: agentID, + payload = CheckInNats{ + Agentid: config.AgentID, Version: version, } case "agent-winsvc": - payload = trmm.WinSvcNats{ - Agentid: agentID, - WinSvcs: services.GetServices(), + svcs, _, _ := services.GetServices() + payload = WinSvcNats{ + Agentid: config.AgentID, + WinSvcs: svcs, } case "agent-agentinfo": osinfo := system.OsString() @@ -119,8 +122,8 @@ func NatsMessage(agentID string, version string, nc *nats.Conn, mode string) { if err != nil { reboot = false } - payload = trmm.AgentInfoNats{ - Agentid: agentID, + payload = AgentInfoNats{ + Agentid: config.AgentID, Username: system.LoggedOnUser(), Hostname: system.GetHostname(), OS: osinfo, @@ -131,48 +134,47 @@ func NatsMessage(agentID string, version string, nc *nats.Conn, mode string) { GoArch: runtime.GOARCH, } case "agent-wmi": - payload = trmm.WinWMINats{ - Agentid: agentID, - WMI: wmi.GetWMIInfo(), + wmiinfo, _ := wmi.GetWMIInfo() + payload = WinWMINats{ + Agentid: config.AgentID, + WMI: wmiinfo, } case "agent-disks": - payload = trmm.WinDisksNats{ - Agentid: agentID, - Disks: disk.GetDisks(), + disks, _ := disk.GetDisks() + payload = WinDisksNats{ + Agentid: config.AgentID, + Disks: disks, } case "agent-publicip": - payload = trmm.PublicIPNats{ - Agentid: agentID, - PublicIP: a.PublicIP(), + payload = PublicIPNats{ + Agentid: config.AgentID, + PublicIP: network.PublicIP(config.Proxy), } } //a.Logger.Debugln(mode, payload) ret.Encode(payload) - nc.PublishRequest(a.AgentID, mode, resp) + nc.PublishRequest(config.AgentID, mode, resp) } -func DoNatsCheckIn() { +func DoNatsCheckIn(version string) { opts := SetupNatsOptions() - server := fmt.Sprintf("tls://%s:4222", a.ApiURL) + server := fmt.Sprintf("tls://%s:4222", config.NewAgentConfig().APIURL) nc, err := nats.Connect(server, opts...) if err != nil { - //a.Logger.Errorln(err) return } for _, s := range natsCheckin { time.Sleep(time.Duration(utils.RandRange(100, 400)) * time.Millisecond) - NatsMessage(nc, s) + NatsMessage(version, nc, s) } + nc.Close() } -func AgentStartup(agentID string) { - url := "/api/v3/checkin/" +func AgentStartup(agentID string) error { payload := map[string]interface{}{"agent_id": agentID} - _, err := tactical.PostRequest(url, payload, 15) - if err != nil { - //a.Logger.Debugln("AgentStartup()", err) - } + err := api.PostPayload(payload, "/api/v3/checkin/") + return err } diff --git a/agent/tactical/service/structs.go b/agent/tactical/service/structs.go new file mode 100644 index 0000000..8795a0d --- /dev/null +++ b/agent/tactical/service/structs.go @@ -0,0 +1,43 @@ +package service + +import ( + "github.com/amidaware/rmmagent/agent/disk" + "github.com/amidaware/rmmagent/agent/services" +) + +type WinSvcNats struct { + Agentid string `json:"agent_id"` + WinSvcs []services.Service `json:"services"` +} + +type CheckInNats struct { + Agentid string `json:"agent_id"` + Version string `json:"version"` +} + +type AgentInfoNats struct { + Agentid string `json:"agent_id"` + Username string `json:"logged_in_username"` + Hostname string `json:"hostname"` + OS string `json:"operating_system"` + Platform string `json:"plat"` + TotalRAM float64 `json:"total_ram"` + BootTime int64 `json:"boot_time"` + RebootNeeded bool `json:"needs_reboot"` + GoArch string `json:"goarch"` +} + +type WinWMINats struct { + Agentid string `json:"agent_id"` + WMI interface{} `json:"wmi"` +} + +type WinDisksNats struct { + Agentid string `json:"agent_id"` + Disks []disk.Disk `json:"disks"` +} + +type PublicIPNats struct { + Agentid string `json:"agent_id"` + PublicIP string `json:"public_ip"` +} diff --git a/agent/tactical/shared/shared.go b/agent/tactical/shared/shared.go new file mode 100644 index 0000000..3263a50 --- /dev/null +++ b/agent/tactical/shared/shared.go @@ -0,0 +1,19 @@ +package shared + +import ( + "github.com/amidaware/rmmagent/agent/software" + "github.com/amidaware/rmmagent/agent/tactical/api" + "github.com/amidaware/rmmagent/agent/tactical/config" +) + +func SendSoftware() error { + config := config.NewAgentConfig() + sw, _ := software.GetInstalledSoftware() + payload := map[string]interface{}{"agent_id": config.AgentID, "software": sw} + err := api.PostPayload(payload, "/api/v3/software/") + if err != nil { + return err + } + + return nil +} diff --git a/agent/tactical/shared/shared_windows.go b/agent/tactical/shared/shared_windows.go new file mode 100644 index 0000000..899d811 --- /dev/null +++ b/agent/tactical/shared/shared_windows.go @@ -0,0 +1,70 @@ +package shared + +import ( + "fmt" + "os" + "path/filepath" + "runtime" + "time" + + "github.com/amidaware/rmmagent/agent/system" + "github.com/amidaware/rmmagent/agent/tactical/config" + "github.com/amidaware/rmmagent/agent/utils" + "github.com/go-resty/resty/v2" +) + +func GetPython(force bool) { + if utils.FileExists(system.GetPythonBin()) && !force { + return + } + + var archZip string + var folder string + switch runtime.GOARCH { + case "amd64": + archZip = "py38-x64.zip" + folder = "py38-x64" + case "386": + archZip = "py38-x32.zip" + folder = "py38-x32" + } + pyFolder := filepath.Join(system.GetProgramDirectory(), folder) + pyZip := filepath.Join(system.GetProgramDirectory(), archZip) + defer os.Remove(pyZip) + + if force { + os.RemoveAll(pyFolder) + } + + config := config.NewAgentConfig() + rClient := resty.New() + rClient.SetTimeout(20 * time.Minute) + rClient.SetRetryCount(10) + rClient.SetRetryWaitTime(1 * time.Minute) + rClient.SetRetryMaxWaitTime(15 * time.Minute) + if len(config.Proxy) > 0 { + rClient.SetProxy(config.Proxy) + } + + url := fmt.Sprintf("https://github.com/amidaware/rmmagent/releases/download/v2.0.0/%s", archZip) + r, err := rClient.R().SetOutput(pyZip).Get(url) + if err != nil { + return + } + if r.IsError() { + return + } + + err = utils.Unzip(pyZip, system.GetProgramDirectory()) + if err != nil { + } +} + +func RunMigrations() { + for _, i := range []string{"nssm.exe", "nssm-x86.exe"} { + nssm := filepath.Join(system.GetProgramDirectory(), i) + if utils.FileExists(nssm) { + os.Remove(nssm) + } + } +} diff --git a/agent/tactical/structs.go b/agent/tactical/structs.go new file mode 100644 index 0000000..1f03ba5 --- /dev/null +++ b/agent/tactical/structs.go @@ -0,0 +1 @@ +package tactical diff --git a/agent/tactical/tactical.go b/agent/tactical/tactical.go index 1f03ba5..f3cd9a9 100644 --- a/agent/tactical/tactical.go +++ b/agent/tactical/tactical.go @@ -1 +1,2 @@ package tactical + diff --git a/agent/tactical/tactical_test.go b/agent/tactical/tactical_test.go index 9dac147..a829c2f 100644 --- a/agent/tactical/tactical_test.go +++ b/agent/tactical/tactical_test.go @@ -1,17 +1,2 @@ -package tactical +package tactical_test -import "testing" - -func TestSyncMeshNodeID(t *testing.T) { - agentConfig := NewAgentConfig() - if agentConfig.AgentID == "" { - t.Fatal("Could not get AgentID") - } - - result := SyncMeshNodeID() - if !result { - t.Fatal("SyncMeshNodeID resulted in error") - } - - t.Log("Synced mesh node id") -} \ No newline at end of file diff --git a/agent/tactical/tactical_windows.go b/agent/tactical/tactical_windows.go index 317388f..2b3726c 100644 --- a/agent/tactical/tactical_windows.go +++ b/agent/tactical/tactical_windows.go @@ -6,7 +6,6 @@ import ( "os" "os/exec" "path/filepath" - "runtime" "strings" "time" @@ -14,13 +13,10 @@ import ( "github.com/amidaware/rmmagent/agent/services" "github.com/amidaware/rmmagent/agent/system" "github.com/amidaware/rmmagent/agent/tactical/config" - "github.com/amidaware/rmmagent/agent/tactical/rpc" "github.com/amidaware/rmmagent/agent/tasks" "github.com/amidaware/rmmagent/agent/utils" rmm "github.com/amidaware/rmmagent/shared" "github.com/go-resty/resty/v2" - "github.com/kardianos/service" - trmm "github.com/wh1te909/trmm-shared" "golang.org/x/sys/windows" "golang.org/x/sys/windows/registry" ) @@ -37,9 +33,6 @@ func AgentUpdate(url, inno, version string) { system.KillHungUpdates() CleanupAgentUpdates() updater := filepath.Join(system.GetProgramDirectory(), inno) - //a.Logger.Infof("Agent updating from %s to %s", a.Version, version) - //a.Logger.Infoln("Downloading agent update from", url) - config := config.NewAgentConfig() rClient := resty.New() rClient.SetCloseConnection(true) @@ -51,20 +44,17 @@ func AgentUpdate(url, inno, version string) { r, err := rClient.R().SetOutput(updater).Get(url) if err != nil { - //a.Logger.Errorln(err) system.CMD("net", []string{"start", services.WinSvcName}, 10, false) return } if r.IsError() { - //a.Logger.Errorln("Download failed with status code", r.StatusCode()) system.CMD("net", []string{"start", services.WinSvcName}, 10, false) return } dir, err := ioutil.TempDir("", "tacticalrmm") if err != nil { - //a.Logger.Errorln("Agentupdate create tempdir:", err) system.CMD("net", []string{"start", services.WinSvcName}, 10, false) return } @@ -84,7 +74,6 @@ func CleanupAgentUpdates() { pd := filepath.Join(os.Getenv("ProgramFiles"), system.ProgFilesName) cderr := os.Chdir(pd) if cderr != nil { - //a.Logger.Errorln(cderr) return } @@ -97,7 +86,6 @@ func CleanupAgentUpdates() { cderr = os.Chdir(os.Getenv("TMP")) if cderr != nil { - //a.Logger.Errorln(cderr) return } @@ -135,122 +123,3 @@ func GetUninstallExe() string { return "unins000.exe" } - -// RunMigrations cleans up unused stuff from older agents -func RunMigrations() { - for _, i := range []string{"nssm.exe", "nssm-x86.exe"} { - nssm := filepath.Join(system.GetProgramDirectory(), i) - if trmm.FileExists(nssm) { - os.Remove(nssm) - } - } -} - -func installMesh(meshbin, exe, proxy string) (string, error) { - var meshNodeID string - meshInstallArgs := []string{"-fullinstall"} - if len(proxy) > 0 { - meshProxy := fmt.Sprintf("--WebProxy=%s", proxy) - meshInstallArgs = append(meshInstallArgs, meshProxy) - } - - //a.Logger.Debugln("Mesh install args:", meshInstallArgs) - meshOut, meshErr := system.CMD(meshbin, meshInstallArgs, int(90), false) - if meshErr != nil { - fmt.Println(meshOut[0]) - fmt.Println(meshOut[1]) - fmt.Println(meshErr) - } - - fmt.Println(meshOut) - //a.Logger.Debugln("Sleeping for 5") - time.Sleep(5 * time.Second) - - meshSuccess := false - - for !meshSuccess { - //a.Logger.Debugln("Getting mesh node id") - pMesh, pErr := system.CMD(exe, []string{"-nodeid"}, int(30), false) - if pErr != nil { - //a.Logger.Errorln(pErr) - time.Sleep(5 * time.Second) - continue - } - - if pMesh[1] != "" { - //a.Logger.Errorln(pMesh[1]) - time.Sleep(5 * time.Second) - continue - } - - meshNodeID = utils.StripAll(pMesh[0]) - //a.Logger.Debugln("Node id:", meshNodeID) - if strings.Contains(strings.ToLower(meshNodeID), "not defined") { - //a.Logger.Errorln(meshNodeID) - time.Sleep(5 * time.Second) - continue - } - - meshSuccess = true - } - - return meshNodeID, nil -} - -func Start(_ service.Service) error { - go rpc.RunRPC(NewAgentConfig()) - return nil -} - -func GetPython(force bool) { - if trmm.FileExists(system.GetPythonBin()) && !force { - return - } - - var archZip string - var folder string - switch runtime.GOARCH { - case "amd64": - archZip = "py38-x64.zip" - folder = "py38-x64" - case "386": - archZip = "py38-x32.zip" - folder = "py38-x32" - } - pyFolder := filepath.Join(system.GetProgramDirectory(), folder) - pyZip := filepath.Join(system.GetProgramDirectory(), archZip) - //a.Logger.Debugln(pyZip) - //a.Logger.Debugln(a.PyBin) - defer os.Remove(pyZip) - - if force { - os.RemoveAll(pyFolder) - } - - config := NewAgentConfig() - rClient := resty.New() - rClient.SetTimeout(20 * time.Minute) - rClient.SetRetryCount(10) - rClient.SetRetryWaitTime(1 * time.Minute) - rClient.SetRetryMaxWaitTime(15 * time.Minute) - if len(config.Proxy) > 0 { - rClient.SetProxy(config.Proxy) - } - - url := fmt.Sprintf("https://github.com/amidaware/rmmagent/releases/download/v2.0.0/%s", archZip) - //a.Logger.Debugln(url) - r, err := rClient.R().SetOutput(pyZip).Get(url) - if err != nil { - //a.Logger.Errorln("Unable to download py3.zip from github.", err) - return - } - if r.IsError() { - //a.Logger.Errorln("Unable to download py3.zip from github. Status code", r.StatusCode()) - return - } - - err = utils.Unzip(pyZip, system.GetProgramDirectory()) - if err != nil { - //a.Logger.Errorln(err) - } -} diff --git a/agent/tactical/tasks/structs.go b/agent/tactical/tasks/structs.go new file mode 100644 index 0000000..8763644 --- /dev/null +++ b/agent/tactical/tasks/structs.go @@ -0,0 +1,25 @@ +package tasks + +type AutomatedTask struct { + ID int `json:"id"` + TaskActions []TaskAction `json:"task_actions"` + Enabled bool `json:"enabled"` + ContinueOnError bool `json:"continue_on_error"` +} + +type TaskAction struct { + ActionType string `json:"type"` + Command string `json:"command"` + Shell string `json:"shell"` + ScriptName string `json:"script_name"` + Code string `json:"code"` + Args []string `json:"script_args"` + Timeout int `json:"timeout"` +} + +type TaskResult struct { + Stdout string `json:"stdout"` + Stderr string `json:"stderr"` + RetCode int `json:"retcode"` + ExecTime float64 `json:"execution_time"` +} diff --git a/agent/tactical/tasks/tasks.go b/agent/tactical/tasks/tasks.go new file mode 100644 index 0000000..a1362ba --- /dev/null +++ b/agent/tactical/tasks/tasks.go @@ -0,0 +1,95 @@ +package tasks + +import ( + "encoding/json" + "fmt" + "time" + + "github.com/amidaware/rmmagent/agent/system" + "github.com/amidaware/rmmagent/agent/tactical/api" + "github.com/amidaware/rmmagent/agent/tactical/config" +) + +func RunTask(id int) error { + config := config.NewAgentConfig() + data := AutomatedTask{} + url := fmt.Sprintf("/api/v3/%d/%s/taskrunner/", id, config.AgentID) + r1, gerr := api.Get(url) + if gerr != nil { + return gerr + } + + if r1.IsError() { + return nil + } + + if err := json.Unmarshal(r1.Body(), &data); err != nil { + return err + } + + start := time.Now() + payload := TaskResult{} + // loop through all task actions + for _, action := range data.TaskActions { + action_start := time.Now() + if action.ActionType == "script" { + stdout, stderr, retcode, err := system.RunScript(action.Code, action.Shell, action.Args, action.Timeout) + + if err != nil { + } + + // add text to stdout showing which script ran if more than 1 script + action_exec_time := time.Since(action_start).Seconds() + + if len(data.TaskActions) > 1 { + payload.Stdout += fmt.Sprintf("\n------------\nRunning Script: %s. Execution Time: %f\n------------\n\n", action.ScriptName, action_exec_time) + } + + // save results + payload.Stdout += stdout + payload.Stderr += stderr + payload.RetCode = retcode + + if !data.ContinueOnError && stderr != "" { + break + } + + } else if action.ActionType == "cmd" { + // out[0] == stdout, out[1] == stderr + out, err := system.CMDShell(action.Shell, []string{}, action.Command, action.Timeout, false) + + if err != nil { + } + + if len(data.TaskActions) > 1 { + action_exec_time := time.Since(action_start).Seconds() + // add text to stdout showing which script ran + payload.Stdout += fmt.Sprintf("\n------------\nRunning Command: %s. Execution Time: %f\n------------\n\n", action.Command, action_exec_time) + } + + // save results + payload.Stdout += out[0] + payload.Stderr += out[1] + // no error + if out[1] == "" { + payload.RetCode = 0 + } else { + payload.RetCode = 1 + + if !data.ContinueOnError { + break + } + } + + } else { + } + } + + payload.ExecTime = time.Since(start).Seconds() + perr := api.Patch(payload, url) + if perr != nil { + return perr + } + + return nil +} diff --git a/main.go b/main.go index 41b47e5..9980df8 100644 --- a/main.go +++ b/main.go @@ -14,17 +14,17 @@ package main import ( "flag" "fmt" + "github.com/amidaware/rmmagent/agent" + "github.com/kardianos/service" + "github.com/sirupsen/logrus" "os" "os/user" "path/filepath" "runtime" - "github.com/amidaware/rmmagent/agent" - "github.com/kardianos/service" - "github.com/sirupsen/logrus" ) var ( - version = "2.0.4" + version = "development" log = logrus.New() logFile *os.File )