diff --git a/README.md b/README.md index bc8ea97..4d68653 100644 --- a/README.md +++ b/README.md @@ -3,12 +3,17 @@ https://github.com/amidaware/tacticalrmm #### building the agent - linux ``` +go install github.com/josephspurrier/goversioninfo/cmd/goversioninfo +go generate env CGO_ENABLED=0 GOOS= GOARCH= go build -ldflags "-s -w -X 'main.version=v2.0.4'" +example: env CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags "-s -w -X 'main.version=v2.0.4' -o build/output/rmmagent" ``` #### building the agent - windows ``` -$env:CGO_ENABLED="0";$env:GOOS="windows";$env:GOARCH="amd64"; go build -ldflags "-s -w -X 'main.version=v2.0.4'" +go install github.com/josephspurrier/goversioninfo/cmd/goversioninfo +go generate +$env:CGO_ENABLED="0";$env:GOOS="windows";$env:GOARCH="amd64"; go build -ldflags "-s -w -X 'main.version=v2.0.4' -o build/output/tacticalrmm.exe" ``` ### tests diff --git a/agent/agent_windows.go b/agent/agent_windows.go index 9df3eb5..3aad884 100644 --- a/agent/agent_windows.go +++ b/agent/agent_windows.go @@ -836,15 +836,15 @@ func (a *Agent) InstallService() error { // skip on first call of inno setup if this is a new install _, err := registry.OpenKey(registry.LOCAL_MACHINE, `SOFTWARE\TacticalRMM`, registry.ALL_ACCESS) if err != nil { - return nil + return err } - s, err := service.New(a, a.ServiceConfig) + svc, err := service.New(a, a.ServiceConfig) if err != nil { return err } - return service.Control(s, "install") + return service.Control(svc, "install") } // TODO add to stub diff --git a/agent/install_test.go b/agent/install_test.go index 0f07b6d..66325ba 100644 --- a/agent/install_test.go +++ b/agent/install_test.go @@ -6,7 +6,32 @@ import ( "github.com/spf13/viper" ) + + func TestInstall(t *testing.T) { + testTable := []struct { + name string + expectedError error + version string + }{ + { + name: "Install", + expectedError: nil, + version: "2.0.4", + }, + { + name: "Install Error", + expectedError: nil, + version: "bad ver", + }, + } + + for _, tt := range testTable { + t.Run(tt.name, func(t *testing.T) { + + }) + } + var ( version = "2.0.4" log = logrus.New() diff --git a/agent/patching/patching_windows.go b/agent/patching/patching_windows.go index 1fad3de..b483dd1 100644 --- a/agent/patching/patching_windows.go +++ b/agent/patching/patching_windows.go @@ -36,8 +36,6 @@ func PatchMgmnt(enable bool) error { return nil } - - func GetUpdates() (PackageList, error) { wuaupdates, err := wua.WUAUpdates("IsInstalled=1 or IsInstalled=0 and Type='Software' and IsHidden=0") packages := []Package{} diff --git a/agent/services/services_windows.go b/agent/services/services_windows.go index 7c774ef..c8be2f3 100644 --- a/agent/services/services_windows.go +++ b/agent/services/services_windows.go @@ -297,3 +297,19 @@ func EditService(name, startupType string) WinSvcResp { return WinSvcResp{Success: true, ErrorMsg: ""} } + +func ServiceExists(name string) (bool, error) { + conn, err := mgr.Connect() + if err != nil { + return false, err + } + + defer conn.Disconnect() + srv, err := conn.OpenService(name) + if err != nil { + return false, err + } + + defer srv.Close() + return true, nil +} diff --git a/agent/system/system.go b/agent/system/system.go index 9eafc85..7a103d8 100644 --- a/agent/system/system.go +++ b/agent/system/system.go @@ -12,6 +12,7 @@ import ( "strconv" "time" + "github.com/amidaware/rmmagent/agent/tactical/shared" "github.com/amidaware/rmmagent/agent/utils" ps "github.com/elastic/go-sysinfo" gocmd "github.com/go-cmd/cmd" @@ -144,7 +145,7 @@ func RunPythonCode(code string, timeout int, args []string) (string, error) { } //a.Logger.Debugln(cmdArgs) - cmd := exec.CommandContext(ctx, GetPythonBin(), cmdArgs...) + cmd := exec.CommandContext(ctx, shared.GetPythonBin(), cmdArgs...) cmd.Stdout = &outb cmd.Stderr = &errb diff --git a/agent/system/system_windows.go b/agent/system/system_windows.go index e417737..d8ee28c 100644 --- a/agent/system/system_windows.go +++ b/agent/system/system_windows.go @@ -8,12 +8,12 @@ import ( "os" "os/exec" "path/filepath" - "runtime" "strings" "sync" "syscall" "time" + "github.com/amidaware/rmmagent/agent/tactical/shared" "github.com/amidaware/rmmagent/agent/utils" ps "github.com/elastic/go-sysinfo" "github.com/go-ole/go-ole" @@ -25,11 +25,6 @@ import ( "golang.org/x/sys/windows/registry" ) -const ( - ProgFilesName = "TacticalAgent" - winExeName = "tacticalrmm.exe" -) - func RunScript(code string, shell string, args []string, timeout int) (stdout, stderr string, exitcode int, e error) { content := []byte(code) dir := filepath.Join(os.TempDir(), "trmm") @@ -74,7 +69,7 @@ func RunScript(code string, shell string, args []string, timeout int) (stdout, s exe = "Powershell" cmdArgs = []string{"-NonInteractive", "-NoProfile", "-ExecutionPolicy", "Bypass", tmpfn.Name()} case "python": - exe = GetPythonBin() + exe = shared.GetPythonBin() cmdArgs = []string{tmpfn.Name()} case "cmd": exe = tmpfn.Name() @@ -250,28 +245,6 @@ func CMDShell(shell string, cmdArgs []string, command string, timeout int, detac nil } -func GetProgramDirectory() string { - pd := filepath.Join(os.Getenv("ProgramFiles"), ProgFilesName) - return pd -} - -func GetProgramBin() string { - exe := filepath.Join(GetProgramDirectory(), winExeName) - return exe -} - -func GetPythonBin() string { - var pybin string - switch runtime.GOARCH { - case "amd64": - pybin = filepath.Join(GetProgramDirectory(), "py38-x64", "python.exe") - case "386": - pybin = filepath.Join(GetProgramDirectory(), "py38-x32", "python.exe") - } - - return pybin -} - // LoggedOnUser returns the first logged on user it finds func LoggedOnUser() string { pyCode := ` @@ -471,7 +444,7 @@ func OsString() string { return osFullName } -func AddDefenderExlusions() error { +func AddDefenderExclusions() error { code := ` Add-MpPreference -ExclusionPath 'C:\Program Files\TacticalAgent\*' Add-MpPreference -ExclusionPath 'C:\Windows\Temp\winagent-v*.exe' @@ -507,4 +480,4 @@ func KillProc(pid int32) error { } return nil -} +} \ No newline at end of file diff --git a/agent/tactical/checks/checks.go b/agent/tactical/checks/checks.go index f8ad34f..78b6f31 100644 --- a/agent/tactical/checks/checks.go +++ b/agent/tactical/checks/checks.go @@ -13,6 +13,7 @@ import ( "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/shared" "github.com/amidaware/rmmagent/agent/utils" ps "github.com/elastic/go-sysinfo" "github.com/go-ping/ping" @@ -25,7 +26,7 @@ func CheckRunner(agentID string) error { for { interval, err := GetCheckInterval(agentID) if err == nil && !ChecksRunning() { - _, err = system.CMD(system.GetProgramBin(), []string{"-m", "checkrunner"}, 600, false) + _, err = system.CMD(shared.GetProgramBin(), []string{"-m", "checkrunner"}, 600, false) if err != nil { return err } @@ -66,7 +67,7 @@ Out: if p.PID == 0 { continue } - if p.Exe != system.GetProgramBin() { + if p.Exe != shared.GetProgramBin() { continue } diff --git a/agent/tactical/install/install_unix.go b/agent/tactical/install/install_unix.go new file mode 100644 index 0000000..b79abe2 --- /dev/null +++ b/agent/tactical/install/install_unix.go @@ -0,0 +1,39 @@ +//go:build !windows +// +build !windows + +package install + +import ( + "github.com/spf13/viper" + "log" +) + +func CheckExistingAndRemove(silent bool) error { + return nil +} + +func CreateAgentConfig(baseurl, agentid, apiurl, token, agentpk, cert, proxy, meshdir string) { + viper.SetConfigType("json") + viper.Set("baseurl", baseurl) + viper.Set("agentid", agentid) + viper.Set("apiurl", apiurl) + viper.Set("token", token) + viper.Set("agentpk", agentpk) + viper.Set("cert", cert) + viper.Set("proxy", proxy) + viper.Set("meshdir", meshdir) + viper.SetConfigPermissions(0660) + configLocation := "/etc/tacticalagent" + + err := viper.SafeWriteConfigAs(configLocation) + + if err != nil { + log.Fatalln("createAgentConfig", err) + } +} + +func DisableSleepHibernate() {} + +func EnablePing() {} + +func EnableRDP() {} diff --git a/agent/tactical/install/install_windows.go b/agent/tactical/install/install_windows.go new file mode 100644 index 0000000..2b34ba9 --- /dev/null +++ b/agent/tactical/install/install_windows.go @@ -0,0 +1,349 @@ +package install + +import ( + "errors" + "fmt" + "io" + "math/rand" + "net/url" + "os" + "path/filepath" + "regexp" + "runtime" + "strconv" + "strings" + "time" + + "github.com/amidaware/rmmagent/agent/patching" + "github.com/amidaware/rmmagent/agent/services" + "github.com/amidaware/rmmagent/agent/system" + "github.com/amidaware/rmmagent/agent/tactical" + "github.com/amidaware/rmmagent/agent/tactical/mesh" + "github.com/amidaware/rmmagent/agent/tactical/service" + "github.com/amidaware/rmmagent/agent/tactical/shared" + "github.com/amidaware/rmmagent/agent/utils" + "github.com/go-resty/resty/v2" + "github.com/gonutz/w32/v2" + ksvc "github.com/kardianos/service" + "github.com/shirou/gopsutil/host" + "golang.org/x/sys/windows/registry" +) + +const winSvcName = "tacticalrmm" + +func Install(i *Installer) error { + CheckExistingAndRemove(i.Silent) + i.Headers = map[string]string{ + "content-type": "application/json", + "Authorization": fmt.Sprintf("Token %s", i.Token), + } + + AgentID := GenerateAgentID() + u, err := url.Parse(i.RMM) + if err != nil { + return err + } + + if u.Scheme != "https" && u.Scheme != "http" { + return errors.New("Invalid URL (must contain https or http)") + } + + // will match either ipv4 , or ipv4:port + var ipPort = regexp.MustCompile(`[0-9]+(?:\.[0-9]+){3}(:[0-9]+)?`) + + // if ipv4:port, strip the port to get ip for salt master + if ipPort.MatchString(u.Host) && strings.Contains(u.Host, ":") { + i.SaltMaster = strings.Split(u.Host, ":")[0] + } else if strings.Contains(u.Host, ":") { + i.SaltMaster = strings.Split(u.Host, ":")[0] + } else { + i.SaltMaster = u.Host + } + + terr := utils.TestTCP(fmt.Sprintf("%s:4222", i.SaltMaster)) + if terr != nil { + return fmt.Errorf("ERROR: Either port 4222 TCP is not open on your RMM, or nats.service is not running.\n\n%s", terr.Error()) + } + + baseURL := u.Scheme + "://" + u.Host + iClient := resty.New() + iClient.SetCloseConnection(true) + iClient.SetTimeout(15 * time.Second) + iClient.SetHeaders(i.Headers) + + // set proxy if applicable + if len(i.Proxy) > 0 { + iClient.SetProxy(i.Proxy) + } + + creds, cerr := iClient.R().Get(fmt.Sprintf("%s/api/v3/installer/", baseURL)) + if cerr != nil { + return cerr + } + + if creds.StatusCode() == 401 { + return errors.New("Installer token has expired. Please generate a new one.") + } + + verPayload := map[string]string{"version": i.Version} + iVersion, ierr := iClient.R().SetBody(verPayload).Post(fmt.Sprintf("%s/api/v3/installer/", baseURL)) + if ierr != nil { + return ierr + } + + if iVersion.StatusCode() != 200 { + return errors.New(DjangoStringResp(iVersion.String())) + } + + rClient := resty.New() + rClient.SetCloseConnection(true) + rClient.SetTimeout(i.Timeout * time.Second) + // set rest knox headers + rClient.SetHeaders(i.Headers) + + // set local cert if applicable + if len(i.Cert) > 0 { + if !utils.FileExists(i.Cert) { + return fmt.Errorf("%s does not exist", i.Cert) + } + + rClient.SetRootCertificate(i.Cert) + } + + if len(i.Proxy) > 0 { + rClient.SetProxy(i.Proxy) + } + + var arch string + switch runtime.GOARCH { + case "x86_64": + arch = "64" + case "x86": + arch = "32" + } + + var installerMeshSystemBin string + if len(i.MeshDir) > 0 { + installerMeshSystemBin = filepath.Join(i.MeshDir, "MeshAgent.exe") + } else { + installerMeshSystemBin = mesh.GetMeshBinLocation() + } + + var meshNodeID string + + if runtime.GOOS == "windows" && !i.NoMesh { + meshPath := filepath.Join(shared.GetProgramDirectory(), "meshagent.exe") + if i.LocalMesh == "" { + payload := map[string]string{"arch": arch, "plat": runtime.GOOS} + r, err := rClient.R().SetBody(payload).SetOutput(meshPath).Post(fmt.Sprintf("%s/api/v3/meshexe/", baseURL)) + if err != nil { + } + if r.StatusCode() != 200 { + } + } else { + err := copyFile(i.LocalMesh, meshPath) + if err != nil { + } + } + + time.Sleep(1 * time.Second) + meshNodeID, err = mesh.InstallMesh(meshPath, installerMeshSystemBin, i.Proxy) + if err != nil { + } + } + + if len(i.MeshNodeID) > 0 { + meshNodeID = i.MeshNodeID + } + + // add agent + host, _ := host.Info() + agentPayload := map[string]interface{}{ + "agent_id": AgentID, + "hostname": host.Hostname, + "site": i.SiteID, + "monitoring_type": i.AgentType, + "mesh_node_id": meshNodeID, + "description": i.Description, + "goarch": runtime.GOARCH, + "plat": runtime.GOOS, + } + + r, err := rClient.R().SetBody(agentPayload).SetResult(&NewAgentResp{}).Post(fmt.Sprintf("%s/api/v3/newagent/", baseURL)) + if err != nil { + } + if r.StatusCode() != 200 { + } + + agentPK := r.Result().(*NewAgentResp).AgentPK + agentToken := r.Result().(*NewAgentResp).Token + CreateAgentConfig(baseURL, AgentID, i.SaltMaster, agentToken, strconv.Itoa(agentPK), i.Cert, i.Proxy, i.MeshDir) + time.Sleep(1 * time.Second) + + time.Sleep(3 * time.Second) + + // check in once + service.DoNatsCheckIn(i.Version) + service.SendSoftware() + utils.CreateTRMMTempDir() + patching.PatchMgmnt(true) + + svcConf := &ksvc.Config{ + Executable: shared.GetProgramBin(), + Name: winSvcName, + DisplayName: "TacticalRMM Agent Service", + Arguments: []string{"-m", "svc"}, + Description: "TacticalRMM Agent Service", + Option: ksvc.KeyValue{ + "StartType": "automatic", + "OnFailure": "restart", + "OnFailureDelayDuration": "5s", + "OnFailureResetPeriod": 10, + }, + } + + err = service.InstallService(winSvcName, service.IService{}, svcConf) + if err != nil { + return err + } + + time.Sleep(1 * time.Second) + out := services.ControlService(winSvcName, "start") + if !out.Success { + return errors.New(out.ErrorMsg) + } + + system.AddDefenderExclusions() + if i.Power { + system.DisableSleepHibernate() + } + + if i.Ping { + system.EnablePing() + } + + if i.RDP { + system.EnableRDP() + } + + return nil +} + +func copyFile(src, dst string) error { + in, err := os.Open(src) + if err != nil { + return err + } + defer in.Close() + + out, err := os.Create(dst) + if err != nil { + return err + } + defer out.Close() + + _, err = io.Copy(out, in) + if err != nil { + return err + } + return nil +} + +// GenerateAgentID creates and returns a unique agent id +func GenerateAgentID() string { + rand.Seed(time.Now().UnixNano()) + letters := []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") + b := make([]rune, 40) + for i := range b { + b[i] = letters[rand.Intn(len(letters))] + } + return string(b) +} + +// DjangoStringResp removes double quotes from django rest api resp +func DjangoStringResp(resp string) string { + return strings.Trim(resp, `"`) +} + +func CreateAgentConfig(baseurl, agentid, apiurl, token, agentpk, cert, proxy, meshdir string) error { + k, _, err := registry.CreateKey(registry.LOCAL_MACHINE, `SOFTWARE\TacticalRMM`, registry.ALL_ACCESS) + if err != nil { + return err + } + + defer k.Close() + err = k.SetStringValue("BaseURL", baseurl) + if err != nil { + return err + } + + err = k.SetStringValue("AgentID", agentid) + if err != nil { + return err + } + + err = k.SetStringValue("ApiURL", apiurl) + if err != nil { + return err + } + + err = k.SetStringValue("Token", token) + if err != nil { + return err + } + + err = k.SetStringValue("AgentPK", agentpk) + if err != nil { + } + + if len(cert) > 0 { + err = k.SetStringValue("Cert", cert) + if err != nil { + return err + } + } + + if len(proxy) > 0 { + err = k.SetStringValue("Proxy", proxy) + if err != nil { + return err + } + } + + if len(meshdir) > 0 { + err = k.SetStringValue("MeshDir", meshdir) + if err != nil { + return err + } + } + + return nil +} + +func CheckExistingAndRemove(silent bool) { + hasReg := false + _, err := registry.OpenKey(registry.LOCAL_MACHINE, `SOFTWARE\TacticalRMM`, registry.ALL_ACCESS) + + if err == nil { + hasReg = true + } + if hasReg { + tacUninst := filepath.Join(shared.GetProgramDirectory(), tactical.GetUninstallExe()) + tacUninstArgs := [2]string{tacUninst, "/VERYSILENT"} + + window := w32.GetForegroundWindow() + if !silent && window != 0 { + var handle w32.HWND + msg := "Existing installation found\nClick OK to remove, then re-run the installer.\nClick Cancel to abort." + action := w32.MessageBox(handle, msg, "Tactical RMM", w32.MB_OKCANCEL|w32.MB_ICONWARNING) + if action == w32.IDOK { + tactical.AgentUninstall("foo") + } + } else { + fmt.Println("Existing installation found and must be removed before attempting to reinstall.") + fmt.Println("Run the following command to uninstall, and then re-run this installer.") + fmt.Printf(`"%s" %s `, tacUninstArgs[0], tacUninstArgs[1]) + } + os.Exit(0) + } +} diff --git a/agent/tactical/install/structs.go b/agent/tactical/install/structs.go new file mode 100644 index 0000000..803108a --- /dev/null +++ b/agent/tactical/install/structs.go @@ -0,0 +1,31 @@ +package install + +import "time" + +type Installer struct { + Headers map[string]string + RMM string + ClientID int + SiteID int + Description string + AgentType string + Power bool + RDP bool + Ping bool + Token string + LocalMesh string + Cert string + Proxy string + Timeout time.Duration + SaltMaster string + Silent bool + NoMesh bool + MeshDir string + MeshNodeID string + Version string +} + +type NewAgentResp struct { + AgentPK int `json:"pk"` + Token string `json:"token"` +} diff --git a/agent/tactical/rpc/rpc.go b/agent/tactical/rpc/rpc.go deleted file mode 100644 index 2bf7c20..0000000 --- a/agent/tactical/rpc/rpc.go +++ /dev/null @@ -1,472 +0,0 @@ -package rpc - -import ( - "fmt" - "os" - "runtime" - "strconv" - "sync" - "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" - ksvc "github.com/kardianos/service" - nats "github.com/nats-io/nats.go" - "github.com/ugorji/go/codec" -) - -var ( - agentUpdateLocker uint32 - getWinUpdateLocker uint32 - installWinUpdateLocker uint32 -) - -func RunRPC(version string) error { - config := config.NewAgentConfig() - go service.RunAsService(version) - var wg sync.WaitGroup - wg.Add(1) - opts := service.SetupNatsOptions() - server := fmt.Sprintf("tls://%s:4222", config.APIURL) - nc, err := nats.Connect(server, opts...) - if err != nil { - return err - } - - 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 { - return - } - - switch payload.Func { - case "ping": - go func() { - var resp []byte - ret := codec.NewEncoderBytes(&resp, new(codec.MsgpackHandle)) - ret.Encode("pong") - msg.Respond(resp) - }() - - case "patchmgmt": - go func(p *NatsMsg) { - var resp []byte - ret := codec.NewEncoderBytes(&resp, new(codec.MsgpackHandle)) - err := patching.PatchMgmnt(p.PatchMgmt) - if err != nil { - ret.Encode(err.Error()) - } else { - ret.Encode("ok") - } - msg.Respond(resp) - }(payload) - - case "schedtask": - go func(p *NatsMsg) { - var resp []byte - ret := codec.NewEncoderBytes(&resp, new(codec.MsgpackHandle)) - success, err := tasks.CreateSchedTask(p.ScheduledTask) - if err != nil { - ret.Encode(err.Error()) - } else if !success { - ret.Encode("Something went wrong") - } else { - ret.Encode("ok") - } - msg.Respond(resp) - }(payload) - - case "delschedtask": - go func(p *NatsMsg) { - var resp []byte - ret := codec.NewEncoderBytes(&resp, new(codec.MsgpackHandle)) - err := tasks.DeleteSchedTask(p.ScheduledTask.Name) - if err != nil { - ret.Encode(err.Error()) - } else { - ret.Encode("ok") - } - msg.Respond(resp) - }(payload) - - case "listschedtasks": - go func() { - var resp []byte - ret := codec.NewEncoderBytes(&resp, new(codec.MsgpackHandle)) - tasks, _ := tasks.ListSchedTasks() - ret.Encode(tasks) - msg.Respond(resp) - }() - - case "eventlog": - go func(p *NatsMsg) { - var resp []byte - ret := codec.NewEncoderBytes(&resp, new(codec.MsgpackHandle)) - days, _ := strconv.Atoi(p.Data["days"]) - evtLog, _ := events.GetEventLog(p.Data["logname"], days) - ret.Encode(evtLog) - msg.Respond(resp) - }(payload) - - case "procs": - go func() { - var resp []byte - ret := codec.NewEncoderBytes(&resp, new(codec.MsgpackHandle)) - procs := system.GetProcsRPC() - ret.Encode(procs) - msg.Respond(resp) - }() - - case "killproc": - go func(p *NatsMsg) { - var resp []byte - ret := codec.NewEncoderBytes(&resp, new(codec.MsgpackHandle)) - err := system.KillProc(p.ProcPID) - if err != nil { - ret.Encode(err.Error()) - } else { - ret.Encode("ok") - } - msg.Respond(resp) - }(payload) - - case "rawcmd": - go func(p *NatsMsg) { - var resp []byte - var resultData RawCMDResp - ret := codec.NewEncoderBytes(&resp, new(codec.MsgpackHandle)) - - switch runtime.GOOS { - case "windows": - out, _ := system.CMDShell(p.Data["shell"], []string{}, p.Data["command"], p.Timeout, false) - if out[1] != "" { - ret.Encode(out[1]) - resultData.Results = out[1] - } else { - ret.Encode(out[0]) - resultData.Results = out[0] - } - default: - opts := system.NewCMDOpts() - opts.Shell = p.Data["shell"] - opts.Command = p.Data["command"] - opts.Timeout = time.Duration(p.Timeout) - out := system.CmdV2(opts) - tmp := "" - if len(out.Stdout) > 0 { - tmp += out.Stdout - } - if len(out.Stderr) > 0 { - tmp += "\n" - tmp += out.Stderr - } - ret.Encode(tmp) - resultData.Results = tmp - } - - msg.Respond(resp) - if p.ID != 0 { - api.Patch(resultData, fmt.Sprintf("/api/v3/%d/%s/histresult/", p.ID, config.AgentID)) - } - }(payload) - - case "winservices": - go func() { - var resp []byte - ret := codec.NewEncoderBytes(&resp, new(codec.MsgpackHandle)) - svcs, _, _ := services.GetServices() - ret.Encode(svcs) - msg.Respond(resp) - }() - - case "winsvcdetail": - go func(p *NatsMsg) { - var resp []byte - ret := codec.NewEncoderBytes(&resp, new(codec.MsgpackHandle)) - svc := services.GetServiceDetail(p.Data["name"]) - ret.Encode(svc) - msg.Respond(resp) - }(payload) - - case "winsvcaction": - go func(p *NatsMsg) { - var resp []byte - ret := codec.NewEncoderBytes(&resp, new(codec.MsgpackHandle)) - retData := services.ControlService(p.Data["name"], p.Data["action"]) - ret.Encode(retData) - msg.Respond(resp) - }(payload) - - case "editwinsvc": - go func(p *NatsMsg) { - var resp []byte - ret := codec.NewEncoderBytes(&resp, new(codec.MsgpackHandle)) - retData := services.EditService(p.Data["name"], p.Data["startType"]) - ret.Encode(retData) - msg.Respond(resp) - }(payload) - - case "runscript": - go func(p *NatsMsg) { - var resp []byte - var retData string - var resultData RunScriptResp - ret := codec.NewEncoderBytes(&resp, new(codec.MsgpackHandle)) - start := time.Now() - 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 { - retData = err.Error() - resultData.Retcode = 1 - resultData.Stderr = err.Error() - } else { - retData = stdout + stderr // to keep backwards compat - resultData.Retcode = retcode - resultData.Stdout = stdout - resultData.Stderr = stderr - } - //a.Logger.Debugln(retData) - ret.Encode(retData) - msg.Respond(resp) - if p.ID != 0 { - results := map[string]interface{}{"script_results": resultData} - 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 RunScriptResp - ret := codec.NewEncoderBytes(&resp, new(codec.MsgpackHandle)) - start := time.Now() - 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 { - retData.Stderr = err.Error() - retData.Retcode = 1 - } else { - retData.Stdout = stdout - retData.Stderr = stderr - retData.Retcode = retcode - } - retData.ID = p.ID - //a.Logger.Debugln(retData) - ret.Encode(retData) - msg.Respond(resp) - if p.ID != 0 { - results := map[string]interface{}{"script_results": retData} - api.Patch(results, fmt.Sprintf("/api/v3/%d/%s/histresult/", p.ID, config.AgentID)) - } - }(payload) - - case "recover": - go func(p *NatsMsg) { - var resp []byte - ret := codec.NewEncoderBytes(&resp, new(codec.MsgpackHandle)) - - switch p.Data["mode"] { - case "mesh": - mesh.RecoverMesh() - } - - ret.Encode("ok") - msg.Respond(resp) - }(payload) - case "softwarelist": - go func() { - var resp []byte - ret := codec.NewEncoderBytes(&resp, new(codec.MsgpackHandle)) - sw, _ := software.GetInstalledSoftware() - ret.Encode(sw) - msg.Respond(resp) - }() - - case "rebootnow": - go func() { - //a.Logger.Debugln("Scheduling immediate reboot") - var resp []byte - ret := codec.NewEncoderBytes(&resp, new(codec.MsgpackHandle)) - ret.Encode("ok") - msg.Respond(resp) - if runtime.GOOS == "windows" { - system.CMD("shutdown.exe", []string{"/r", "/t", "5", "/f"}, 15, false) - } else { - opts := system.NewCMDOpts() - opts.Command = "reboot" - system.CmdV2(opts) - } - }() - case "needsreboot": - go func() { - var resp []byte - ret := codec.NewEncoderBytes(&resp, new(codec.MsgpackHandle)) - out, err := system.SystemRebootRequired() - if err == nil { - ret.Encode(out) - } else { - ret.Encode(false) - } - msg.Respond(resp) - }() - case "sysinfo": - go func() { - var resp []byte - ret := codec.NewEncoderBytes(&resp, new(codec.MsgpackHandle)) - modes := []string{"agent-agentinfo", "agent-disks", "agent-wmi", "agent-publicip"} - for _, m := range modes { - service.NatsMessage(version, nc, m) - } - ret.Encode("ok") - msg.Respond(resp) - }() - case "wmi": - go func() { - service.NatsMessage(version, nc, "agent-wmi") - }() - case "cpuloadavg": - go func() { - var resp []byte - ret := codec.NewEncoderBytes(&resp, new(codec.MsgpackHandle)) - loadAvg := system.GetCPULoadAvg() - ret.Encode(loadAvg) - msg.Respond(resp) - }() - case "runchecks": - go func() { - var resp []byte - ret := codec.NewEncoderBytes(&resp, new(codec.MsgpackHandle)) - if runtime.GOOS == "windows" { - if checks.ChecksRunning() { - ret.Encode("busy") - msg.Respond(resp) - } else { - ret.Encode("ok") - msg.Respond(resp) - _, checkerr := system.CMD(system.GetProgramBin(), []string{"-m", "runchecks"}, 600, false) - if checkerr != nil { - } - } - } else { - ret.Encode("ok") - msg.Respond(resp) - checks.RunChecks(config.AgentID, true) - } - - }() - case "runtask": - go func(p *NatsMsg) { - ttasks.RunTask(p.TaskPK) - }(payload) - - case "publicip": - go func() { - var resp []byte - ret := codec.NewEncoderBytes(&resp, new(codec.MsgpackHandle)) - ret.Encode(network.PublicIP(config.Proxy)) - msg.Respond(resp) - }() - case "installpython": - go shared.GetPython(true) - case "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, _ := choco.InstallWithChoco(p.ChocoProgName) - results := map[string]string{"results": out} - url := fmt.Sprintf("/api/v3/%d/chocoresult/", p.PendingActionPK) - api.Patch(results, url) - }(payload) - case "getwinupdates": - go func() { - if !atomic.CompareAndSwapUint32(&getWinUpdateLocker, 0, 1) { - //a.Logger.Debugln("Already checking for windows updates") - } else { - //a.Logger.Debugln("Checking for windows updates") - defer atomic.StoreUint32(&getWinUpdateLocker, 0) - patching.GetUpdates() - } - }() - case "installwinupdates": - go func(p *NatsMsg) { - if !atomic.CompareAndSwapUint32(&installWinUpdateLocker, 0, 1) { - //a.Logger.Debugln("Already installing windows updates") - } else { - //a.Logger.Debugln("Installing windows updates", p.UpdateGUIDs) - defer atomic.StoreUint32(&installWinUpdateLocker, 0) - patching.InstallUpdates(p.UpdateGUIDs) - } - }(payload) - case "agentupdate": - go func(p *NatsMsg) { - var resp []byte - ret := codec.NewEncoderBytes(&resp, new(codec.MsgpackHandle)) - if !atomic.CompareAndSwapUint32(&agentUpdateLocker, 0, 1) { - //a.Logger.Debugln("Agent update already running") - ret.Encode("updaterunning") - msg.Respond(resp) - } else { - ret.Encode("ok") - msg.Respond(resp) - tactical.AgentUpdate(p.Data["url"], p.Data["inno"], p.Data["version"]) - atomic.StoreUint32(&agentUpdateLocker, 0) - nc.Flush() - nc.Close() - os.Exit(0) - } - }(payload) - - case "uninstall": - go func(p *NatsMsg) { - var resp []byte - ret := codec.NewEncoderBytes(&resp, new(codec.MsgpackHandle)) - ret.Encode("ok") - msg.Respond(resp) - tactical.AgentUninstall(p.Code) - nc.Flush() - nc.Close() - os.Exit(0) - }(payload) - } - }) - nc.Flush() - - if err := nc.LastError(); err != nil { - return err - } - - wg.Wait() - - return nil -} - -func Start(version string, _ ksvc.Service) error { - go RunRPC(version) - return nil -} diff --git a/agent/tactical/rpc/rpc_test.go b/agent/tactical/rpc/rpc_test.go deleted file mode 100644 index 6213bfa..0000000 --- a/agent/tactical/rpc/rpc_test.go +++ /dev/null @@ -1,31 +0,0 @@ -package rpc_test - -import ( - "errors" - "testing" - - "github.com/amidaware/rmmagent/agent/tactical/rpc" -) - -func TestRunRPC(t *testing.T) { - testTable := []struct { - name string - expectedError error - version string - }{ - { - name: "Run RPC", - expectedError: nil, - version: "development", - }, - } - - for _, tt := range testTable { - t.Run(tt.name, func(t *testing.T) { - err := rpc.RunRPC(tt.version) - if !errors.Is(tt.expectedError, err) { - t.Errorf("expected (%v), got (%v)", tt.expectedError, err) - } - }) - } -} \ No newline at end of file diff --git a/agent/tactical/rpc/structs.go b/agent/tactical/rpc/structs.go deleted file mode 100644 index 596d047..0000000 --- a/agent/tactical/rpc/structs.go +++ /dev/null @@ -1,32 +0,0 @@ -package rpc - -import "github.com/amidaware/rmmagent/agent/tasks" - -type NatsMsg struct { - Func string `json:"func"` - Timeout int `json:"timeout"` - Data map[string]string `json:"payload"` - ScriptArgs []string `json:"script_args"` - ProcPID int32 `json:"procpid"` - TaskPK int `json:"taskpk"` - ScheduledTask tasks.SchedTask `json:"schedtaskpayload"` - RecoveryCommand string `json:"recoverycommand"` - UpdateGUIDs []string `json:"guids"` - ChocoProgName string `json:"choco_prog_name"` - PendingActionPK int `json:"pending_action_pk"` - PatchMgmt bool `json:"patch_mgmt"` - ID int `json:"id"` - Code string `json:"code"` -} - -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 db0ca4d..f6ecd81 100644 --- a/agent/tactical/service/service.go +++ b/agent/tactical/service/service.go @@ -2,20 +2,473 @@ package service import ( "fmt" + "os" + "runtime" + "strconv" "sync" + "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/shared" + ttasks "github.com/amidaware/rmmagent/agent/tactical/tasks" + "github.com/amidaware/rmmagent/agent/tasks" "github.com/amidaware/rmmagent/agent/utils" + ksvc "github.com/kardianos/service" "github.com/nats-io/nats.go" + "github.com/ugorji/go/codec" +) + +var ( + agentUpdateLocker uint32 + getWinUpdateLocker uint32 + installWinUpdateLocker uint32 ) var natsCheckin = []string{"agent-hello", "agent-agentinfo", "agent-disks", "agent-winsvc", "agent-publicip", "agent-wmi"} +func RunRPC() error { + version := tactical.GetVersion() + config := config.NewAgentConfig() + go RunAsService(version) + var wg sync.WaitGroup + wg.Add(1) + opts := SetupNatsOptions() + server := fmt.Sprintf("tls://%s:4222", config.APIURL) + nc, err := nats.Connect(server, opts...) + if err != nil { + return err + } + + 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 { + return + } + + switch payload.Func { + case "ping": + go func() { + var resp []byte + ret := codec.NewEncoderBytes(&resp, new(codec.MsgpackHandle)) + ret.Encode("pong") + msg.Respond(resp) + }() + + case "patchmgmt": + go func(p *NatsMsg) { + var resp []byte + ret := codec.NewEncoderBytes(&resp, new(codec.MsgpackHandle)) + err := patching.PatchMgmnt(p.PatchMgmt) + if err != nil { + ret.Encode(err.Error()) + } else { + ret.Encode("ok") + } + msg.Respond(resp) + }(payload) + + case "schedtask": + go func(p *NatsMsg) { + var resp []byte + ret := codec.NewEncoderBytes(&resp, new(codec.MsgpackHandle)) + success, err := tasks.CreateSchedTask(p.ScheduledTask) + if err != nil { + ret.Encode(err.Error()) + } else if !success { + ret.Encode("Something went wrong") + } else { + ret.Encode("ok") + } + msg.Respond(resp) + }(payload) + + case "delschedtask": + go func(p *NatsMsg) { + var resp []byte + ret := codec.NewEncoderBytes(&resp, new(codec.MsgpackHandle)) + err := tasks.DeleteSchedTask(p.ScheduledTask.Name) + if err != nil { + ret.Encode(err.Error()) + } else { + ret.Encode("ok") + } + msg.Respond(resp) + }(payload) + + case "listschedtasks": + go func() { + var resp []byte + ret := codec.NewEncoderBytes(&resp, new(codec.MsgpackHandle)) + tasks, _ := tasks.ListSchedTasks() + ret.Encode(tasks) + msg.Respond(resp) + }() + + case "eventlog": + go func(p *NatsMsg) { + var resp []byte + ret := codec.NewEncoderBytes(&resp, new(codec.MsgpackHandle)) + days, _ := strconv.Atoi(p.Data["days"]) + evtLog, _ := events.GetEventLog(p.Data["logname"], days) + ret.Encode(evtLog) + msg.Respond(resp) + }(payload) + + case "procs": + go func() { + var resp []byte + ret := codec.NewEncoderBytes(&resp, new(codec.MsgpackHandle)) + procs := system.GetProcsRPC() + ret.Encode(procs) + msg.Respond(resp) + }() + + case "killproc": + go func(p *NatsMsg) { + var resp []byte + ret := codec.NewEncoderBytes(&resp, new(codec.MsgpackHandle)) + err := system.KillProc(p.ProcPID) + if err != nil { + ret.Encode(err.Error()) + } else { + ret.Encode("ok") + } + msg.Respond(resp) + }(payload) + + case "rawcmd": + go func(p *NatsMsg) { + var resp []byte + var resultData RawCMDResp + ret := codec.NewEncoderBytes(&resp, new(codec.MsgpackHandle)) + + switch runtime.GOOS { + case "windows": + out, _ := system.CMDShell(p.Data["shell"], []string{}, p.Data["command"], p.Timeout, false) + if out[1] != "" { + ret.Encode(out[1]) + resultData.Results = out[1] + } else { + ret.Encode(out[0]) + resultData.Results = out[0] + } + default: + opts := system.NewCMDOpts() + opts.Shell = p.Data["shell"] + opts.Command = p.Data["command"] + opts.Timeout = time.Duration(p.Timeout) + out := system.CmdV2(opts) + tmp := "" + if len(out.Stdout) > 0 { + tmp += out.Stdout + } + if len(out.Stderr) > 0 { + tmp += "\n" + tmp += out.Stderr + } + ret.Encode(tmp) + resultData.Results = tmp + } + + msg.Respond(resp) + if p.ID != 0 { + api.Patch(resultData, fmt.Sprintf("/api/v3/%d/%s/histresult/", p.ID, config.AgentID)) + } + }(payload) + + case "winservices": + go func() { + var resp []byte + ret := codec.NewEncoderBytes(&resp, new(codec.MsgpackHandle)) + svcs, _, _ := services.GetServices() + ret.Encode(svcs) + msg.Respond(resp) + }() + + case "winsvcdetail": + go func(p *NatsMsg) { + var resp []byte + ret := codec.NewEncoderBytes(&resp, new(codec.MsgpackHandle)) + svc := services.GetServiceDetail(p.Data["name"]) + ret.Encode(svc) + msg.Respond(resp) + }(payload) + + case "winsvcaction": + go func(p *NatsMsg) { + var resp []byte + ret := codec.NewEncoderBytes(&resp, new(codec.MsgpackHandle)) + retData := services.ControlService(p.Data["name"], p.Data["action"]) + ret.Encode(retData) + msg.Respond(resp) + }(payload) + + case "editwinsvc": + go func(p *NatsMsg) { + var resp []byte + ret := codec.NewEncoderBytes(&resp, new(codec.MsgpackHandle)) + retData := services.EditService(p.Data["name"], p.Data["startType"]) + ret.Encode(retData) + msg.Respond(resp) + }(payload) + + case "runscript": + go func(p *NatsMsg) { + var resp []byte + var retData string + var resultData RunScriptResp + ret := codec.NewEncoderBytes(&resp, new(codec.MsgpackHandle)) + start := time.Now() + 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 { + retData = err.Error() + resultData.Retcode = 1 + resultData.Stderr = err.Error() + } else { + retData = stdout + stderr // to keep backwards compat + resultData.Retcode = retcode + resultData.Stdout = stdout + resultData.Stderr = stderr + } + //a.Logger.Debugln(retData) + ret.Encode(retData) + msg.Respond(resp) + if p.ID != 0 { + results := map[string]interface{}{"script_results": resultData} + 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 RunScriptResp + ret := codec.NewEncoderBytes(&resp, new(codec.MsgpackHandle)) + start := time.Now() + 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 { + retData.Stderr = err.Error() + retData.Retcode = 1 + } else { + retData.Stdout = stdout + retData.Stderr = stderr + retData.Retcode = retcode + } + retData.ID = p.ID + //a.Logger.Debugln(retData) + ret.Encode(retData) + msg.Respond(resp) + if p.ID != 0 { + results := map[string]interface{}{"script_results": retData} + api.Patch(results, fmt.Sprintf("/api/v3/%d/%s/histresult/", p.ID, config.AgentID)) + } + }(payload) + + case "recover": + go func(p *NatsMsg) { + var resp []byte + ret := codec.NewEncoderBytes(&resp, new(codec.MsgpackHandle)) + + switch p.Data["mode"] { + case "mesh": + mesh.RecoverMesh() + } + + ret.Encode("ok") + msg.Respond(resp) + }(payload) + case "softwarelist": + go func() { + var resp []byte + ret := codec.NewEncoderBytes(&resp, new(codec.MsgpackHandle)) + sw, _ := software.GetInstalledSoftware() + ret.Encode(sw) + msg.Respond(resp) + }() + + case "rebootnow": + go func() { + //a.Logger.Debugln("Scheduling immediate reboot") + var resp []byte + ret := codec.NewEncoderBytes(&resp, new(codec.MsgpackHandle)) + ret.Encode("ok") + msg.Respond(resp) + if runtime.GOOS == "windows" { + system.CMD("shutdown.exe", []string{"/r", "/t", "5", "/f"}, 15, false) + } else { + opts := system.NewCMDOpts() + opts.Command = "reboot" + system.CmdV2(opts) + } + }() + case "needsreboot": + go func() { + var resp []byte + ret := codec.NewEncoderBytes(&resp, new(codec.MsgpackHandle)) + out, err := system.SystemRebootRequired() + if err == nil { + ret.Encode(out) + } else { + ret.Encode(false) + } + msg.Respond(resp) + }() + case "sysinfo": + go func() { + var resp []byte + ret := codec.NewEncoderBytes(&resp, new(codec.MsgpackHandle)) + modes := []string{"agent-agentinfo", "agent-disks", "agent-wmi", "agent-publicip"} + for _, m := range modes { + NatsMessage(version, nc, m) + } + ret.Encode("ok") + msg.Respond(resp) + }() + case "wmi": + go func() { + NatsMessage(version, nc, "agent-wmi") + }() + case "cpuloadavg": + go func() { + var resp []byte + ret := codec.NewEncoderBytes(&resp, new(codec.MsgpackHandle)) + loadAvg := system.GetCPULoadAvg() + ret.Encode(loadAvg) + msg.Respond(resp) + }() + case "runchecks": + go func() { + var resp []byte + ret := codec.NewEncoderBytes(&resp, new(codec.MsgpackHandle)) + if runtime.GOOS == "windows" { + if checks.ChecksRunning() { + ret.Encode("busy") + msg.Respond(resp) + } else { + ret.Encode("ok") + msg.Respond(resp) + _, checkerr := system.CMD(shared.GetProgramBin(), []string{"-m", "runchecks"}, 600, false) + if checkerr != nil { + } + } + } else { + ret.Encode("ok") + msg.Respond(resp) + checks.RunChecks(config.AgentID, true) + } + + }() + case "runtask": + go func(p *NatsMsg) { + ttasks.RunTask(p.TaskPK) + }(payload) + + case "publicip": + go func() { + var resp []byte + ret := codec.NewEncoderBytes(&resp, new(codec.MsgpackHandle)) + ret.Encode(network.PublicIP(config.Proxy)) + msg.Respond(resp) + }() + case "installpython": + go shared.GetPython(true) + case "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, _ := choco.InstallWithChoco(p.ChocoProgName) + results := map[string]string{"results": out} + url := fmt.Sprintf("/api/v3/%d/chocoresult/", p.PendingActionPK) + api.Patch(results, url) + }(payload) + case "getwinupdates": + go func() { + if !atomic.CompareAndSwapUint32(&getWinUpdateLocker, 0, 1) { + //a.Logger.Debugln("Already checking for windows updates") + } else { + //a.Logger.Debugln("Checking for windows updates") + defer atomic.StoreUint32(&getWinUpdateLocker, 0) + patching.GetUpdates() + } + }() + case "installwinupdates": + go func(p *NatsMsg) { + if !atomic.CompareAndSwapUint32(&installWinUpdateLocker, 0, 1) { + //a.Logger.Debugln("Already installing windows updates") + } else { + //a.Logger.Debugln("Installing windows updates", p.UpdateGUIDs) + defer atomic.StoreUint32(&installWinUpdateLocker, 0) + patching.InstallUpdates(p.UpdateGUIDs) + } + }(payload) + case "agentupdate": + go func(p *NatsMsg) { + var resp []byte + ret := codec.NewEncoderBytes(&resp, new(codec.MsgpackHandle)) + if !atomic.CompareAndSwapUint32(&agentUpdateLocker, 0, 1) { + //a.Logger.Debugln("Agent update already running") + ret.Encode("updaterunning") + msg.Respond(resp) + } else { + ret.Encode("ok") + msg.Respond(resp) + tactical.AgentUpdate(p.Data["url"], p.Data["inno"], p.Data["version"]) + atomic.StoreUint32(&agentUpdateLocker, 0) + nc.Flush() + nc.Close() + os.Exit(0) + } + }(payload) + + case "uninstall": + go func(p *NatsMsg) { + var resp []byte + ret := codec.NewEncoderBytes(&resp, new(codec.MsgpackHandle)) + ret.Encode("ok") + msg.Respond(resp) + tactical.AgentUninstall(p.Code) + nc.Flush() + nc.Close() + os.Exit(0) + }(payload) + } + }) + nc.Flush() + + if err := nc.LastError(); err != nil { + return err + } + + wg.Wait() + + return nil +} + func RunAsService(version string) { var wg sync.WaitGroup wg.Add(1) @@ -47,7 +500,7 @@ func AgentSvc(version string) error { time.Sleep(time.Duration(utils.RandRange(1, 3)) * time.Second) AgentStartup(config.AgentID) - shared.SendSoftware() + SendSoftware() checkInHelloTicker := time.NewTicker(time.Duration(utils.RandRange(30, 60)) * time.Second) checkInAgentInfoTicker := time.NewTicker(time.Duration(utils.RandRange(200, 400)) * time.Second) @@ -71,7 +524,7 @@ func AgentSvc(version string) error { case <-checkInDisksTicker.C: NatsMessage(version, nc, "agent-disks") case <-checkInSWTicker.C: - shared.SendSoftware() + SendSoftware() case <-checkInWMITicker.C: NatsMessage(version, nc, "agent-wmi") case <-syncMeshTicker.C: @@ -113,3 +566,22 @@ func AgentStartup(agentID string) error { err := api.PostPayload(payload, "/api/v3/checkin/") return err } + +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 +} + +func (r IService) Start(_ ksvc.Service) error { + go RunRPC() + return nil +} + +func (r IService) Stop(_ ksvc.Service) error { return nil } diff --git a/agent/tactical/service/service_windows.go b/agent/tactical/service/service_windows.go index cc02986..329dfdb 100644 --- a/agent/tactical/service/service_windows.go +++ b/agent/tactical/service/service_windows.go @@ -9,8 +9,10 @@ import ( "github.com/amidaware/rmmagent/agent/system" "github.com/amidaware/rmmagent/agent/tactical/config" "github.com/amidaware/rmmagent/agent/wmi" + ksvc "github.com/kardianos/service" "github.com/nats-io/nats.go" "github.com/ugorji/go/codec" + "golang.org/x/sys/windows/registry" ) func NatsMessage(version string, nc *nats.Conn, mode string) { @@ -69,4 +71,24 @@ func NatsMessage(version string, nc *nats.Conn, mode string) { ret.Encode(payload) nc.PublishRequest(config.AgentID, mode, resp) -} \ No newline at end of file +} + +func InstallService(name string, svc IService, config *ksvc.Config) error { + exists, err := services.ServiceExists(name) + if exists { + return nil + } + + // skip on first call of inno setup if this is a new install + _, err = registry.OpenKey(registry.LOCAL_MACHINE, `SOFTWARE\TacticalRMM`, registry.ALL_ACCESS) + if err != nil { + return nil + } + + s, err := ksvc.New(svc, config) + if err != nil { + return err + } + + return ksvc.Control(s, "install") +} diff --git a/agent/tactical/service/structs.go b/agent/tactical/service/structs.go index 8795a0d..b3ba377 100644 --- a/agent/tactical/service/structs.go +++ b/agent/tactical/service/structs.go @@ -3,8 +3,11 @@ package service import ( "github.com/amidaware/rmmagent/agent/disk" "github.com/amidaware/rmmagent/agent/services" + "github.com/amidaware/rmmagent/agent/tasks" ) +type IService struct{} + type WinSvcNats struct { Agentid string `json:"agent_id"` WinSvcs []services.Service `json:"services"` @@ -41,3 +44,32 @@ type PublicIPNats struct { Agentid string `json:"agent_id"` PublicIP string `json:"public_ip"` } + +type NatsMsg struct { + Func string `json:"func"` + Timeout int `json:"timeout"` + Data map[string]string `json:"payload"` + ScriptArgs []string `json:"script_args"` + ProcPID int32 `json:"procpid"` + TaskPK int `json:"taskpk"` + ScheduledTask tasks.SchedTask `json:"schedtaskpayload"` + RecoveryCommand string `json:"recoverycommand"` + UpdateGUIDs []string `json:"guids"` + ChocoProgName string `json:"choco_prog_name"` + PendingActionPK int `json:"pending_action_pk"` + PatchMgmt bool `json:"patch_mgmt"` + ID int `json:"id"` + Code string `json:"code"` +} + +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/shared/shared.go b/agent/tactical/shared/shared.go deleted file mode 100644 index c620a33..0000000 --- a/agent/tactical/shared/shared.go +++ /dev/null @@ -1,19 +0,0 @@ -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 -} \ No newline at end of file diff --git a/agent/tactical/shared/shared_windows.go b/agent/tactical/shared/shared_windows.go index eaad381..e5d1957 100644 --- a/agent/tactical/shared/shared_windows.go +++ b/agent/tactical/shared/shared_windows.go @@ -7,14 +7,18 @@ import ( "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" ) +const ( + ProgFilesName = "TacticalAgent" + winExeName = "tacticalrmm.exe" +) + func GetPython(force bool) { - if utils.FileExists(system.GetPythonBin()) && !force { + if utils.FileExists(GetPythonBin()) && !force { return } @@ -28,8 +32,8 @@ func GetPython(force bool) { archZip = "py38-x32.zip" folder = "py38-x32" } - pyFolder := filepath.Join(system.GetProgramDirectory(), folder) - pyZip := filepath.Join(system.GetProgramDirectory(), archZip) + pyFolder := filepath.Join(GetProgramDirectory(), folder) + pyZip := filepath.Join(GetProgramDirectory(), archZip) defer os.Remove(pyZip) if force { @@ -55,16 +59,38 @@ func GetPython(force bool) { return } - err = utils.Unzip(pyZip, system.GetProgramDirectory()) + err = utils.Unzip(pyZip, GetProgramDirectory()) if err != nil { } } func RunMigrations() { for _, i := range []string{"nssm.exe", "nssm-x86.exe"} { - nssm := filepath.Join(system.GetProgramDirectory(), i) + nssm := filepath.Join(GetProgramDirectory(), i) if utils.FileExists(nssm) { os.Remove(nssm) } } -} \ No newline at end of file +} + +func GetPythonBin() string { + var pybin string + switch runtime.GOARCH { + case "amd64": + pybin = filepath.Join(GetProgramDirectory(), "py38-x64", "python.exe") + case "386": + pybin = filepath.Join(GetProgramDirectory(), "py38-x32", "python.exe") + } + + return pybin +} + +func GetProgramDirectory() string { + pd := filepath.Join(os.Getenv("ProgramFiles"), ProgFilesName) + return pd +} + +func GetProgramBin() string { + exe := filepath.Join(GetProgramDirectory(), winExeName) + return exe +} diff --git a/agent/tactical/tactical.go b/agent/tactical/tactical.go deleted file mode 100644 index f3cd9a9..0000000 --- a/agent/tactical/tactical.go +++ /dev/null @@ -1,2 +0,0 @@ -package tactical - diff --git a/agent/tactical/tactical_test.go b/agent/tactical/tactical_test.go index a829c2f..348e178 100644 --- a/agent/tactical/tactical_test.go +++ b/agent/tactical/tactical_test.go @@ -1,2 +1,12 @@ package tactical_test +import ( + "testing" + + "github.com/amidaware/rmmagent/agent/tactical" +) + +func TestGetVersion(t *testing.T) { + version := tactical.GetVersion() + t.Logf("got version %s", version) +} diff --git a/agent/tactical/tactical_windows.go b/agent/tactical/tactical_windows.go index 2b3726c..b2af13a 100644 --- a/agent/tactical/tactical_windows.go +++ b/agent/tactical/tactical_windows.go @@ -13,14 +13,34 @@ 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/shared" "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/gonutz/w32/v2" "golang.org/x/sys/windows" "golang.org/x/sys/windows/registry" ) +func GetVersion() string { + path := shared.GetProgramBin() + size := w32.GetFileVersionInfoSize(path) + info := make([]byte, size) + w32.GetFileVersionInfo(path, info) + fixed, _ := w32.VerQueryValueRoot(info) + version := fixed.FileVersion() + stringVersion := fmt.Sprintf( + "%d.%d.%d", + version&0xFFFF000000000000>>48, + version&0x0000FFFF00000000>>32, + version&0x00000000FFFF0000>>16, + //drop last digit version&0x000000000000FFFF>>0, + ) + + return stringVersion +} + func UninstallCleanup() { registry.DeleteKey(registry.LOCAL_MACHINE, `SOFTWARE\TacticalRMM`) patching.PatchMgmnt(false) @@ -32,7 +52,7 @@ func AgentUpdate(url, inno, version string) { time.Sleep(time.Duration(utils.RandRange(1, 15)) * time.Second) system.KillHungUpdates() CleanupAgentUpdates() - updater := filepath.Join(system.GetProgramDirectory(), inno) + updater := filepath.Join(shared.GetProgramDirectory(), inno) config := config.NewAgentConfig() rClient := resty.New() rClient.SetCloseConnection(true) @@ -71,7 +91,7 @@ func AgentUpdate(url, inno, version string) { } func CleanupAgentUpdates() { - pd := filepath.Join(os.Getenv("ProgramFiles"), system.ProgFilesName) + pd := filepath.Join(os.Getenv("ProgramFiles"), shared.ProgFilesName) cderr := os.Chdir(pd) if cderr != nil { return @@ -99,7 +119,7 @@ func CleanupAgentUpdates() { func AgentUninstall(code string) { system.KillHungUpdates() - tacUninst := filepath.Join(system.GetProgramDirectory(), GetUninstallExe()) + tacUninst := filepath.Join(shared.GetProgramDirectory(), GetUninstallExe()) args := []string{"/C", tacUninst, "/VERYSILENT"} cmd := exec.Command("cmd.exe", args...) cmd.SysProcAttr = &windows.SysProcAttr{ @@ -109,7 +129,7 @@ func AgentUninstall(code string) { } func GetUninstallExe() string { - cderr := os.Chdir(system.GetProgramDirectory()) + cderr := os.Chdir(shared.GetProgramDirectory()) if cderr == nil { files, err := filepath.Glob("unins*.exe") if err == nil { diff --git a/agent/utils/utils.go b/agent/utils/utils.go index c9d7af6..68e7f4e 100644 --- a/agent/utils/utils.go +++ b/agent/utils/utils.go @@ -6,6 +6,7 @@ import ( "fmt" "io" "math/rand" + "net" "os" "path/filepath" "strings" @@ -186,3 +187,13 @@ func FileExists(path string) bool { } return true } + +func TestTCP(addr string) error { + conn, err := net.Dial("tcp4", addr) + if err != nil { + return err + } + + defer conn.Close() + return nil +} diff --git a/main.go b/main.go index 9980df8..6b7b8b6 100644 --- a/main.go +++ b/main.go @@ -1,3 +1,5 @@ +//go:generate goversioninfo + /* Copyright 2022 AmidaWare LLC.