This commit is contained in:
wh1te909 2022-03-19 11:55:43 -07:00
commit e455af7f1f
33 changed files with 8471 additions and 0 deletions

470
agent/agent.go Normal file
View file

@ -0,0 +1,470 @@
/*
Copyright 2022 AmidaWare LLC.
Licensed under the Tactical RMM License Version 1.0 (the License).
You may only use the Licensed Software in accordance with the License.
A copy of the License is available at:
https://license.tacticalrmm.com
*/
package agent
import (
"bytes"
"context"
"errors"
"fmt"
"io/ioutil"
"math"
"os"
"os/exec"
"path/filepath"
"runtime"
"strconv"
"strings"
"time"
rmm "github.com/amidaware/rmmagent/shared"
ps "github.com/elastic/go-sysinfo"
gocmd "github.com/go-cmd/cmd"
"github.com/go-resty/resty/v2"
"github.com/kardianos/service"
nats "github.com/nats-io/nats.go"
"github.com/shirou/gopsutil/v3/cpu"
"github.com/sirupsen/logrus"
trmm "github.com/wh1te909/trmm-shared"
)
// Agent struct
type Agent struct {
Hostname string
Arch string
AgentID string
BaseURL string
ApiURL string
Token string
AgentPK int
Cert string
ProgramDir string
EXE string
SystemDrive string
MeshInstaller string
MeshSystemEXE string
MeshSVC string
PyBin string
Headers map[string]string
Logger *logrus.Logger
Version string
Debug bool
rClient *resty.Client
Proxy string
LogTo string
LogFile *os.File
Platform string
GoArch string
ServiceConfig *service.Config
}
const (
progFilesName = "TacticalAgent"
winExeName = "tacticalrmm.exe"
winSvcName = "tacticalrmm"
meshSvcName = "mesh agent"
)
var natsCheckin = []string{"agent-hello", "agent-agentinfo", "agent-disks", "agent-winsvc", "agent-publicip", "agent-wmi"}
func New(logger *logrus.Logger, version string) *Agent {
host, _ := ps.Host()
info := host.Info()
pd := filepath.Join(os.Getenv("ProgramFiles"), progFilesName)
exe := filepath.Join(pd, winExeName)
sd := os.Getenv("SystemDrive")
var pybin string
switch runtime.GOARCH {
case "amd64":
pybin = filepath.Join(pd, "py38-x64", "python.exe")
case "386":
pybin = filepath.Join(pd, "py38-x32", "python.exe")
}
ac := NewAgentConfig()
headers := make(map[string]string)
if len(ac.Token) > 0 {
headers["Content-Type"] = "application/json"
headers["Authorization"] = fmt.Sprintf("Token %s", ac.Token)
}
restyC := resty.New()
restyC.SetBaseURL(ac.BaseURL)
restyC.SetCloseConnection(true)
restyC.SetHeaders(headers)
restyC.SetTimeout(15 * time.Second)
restyC.SetDebug(logger.IsLevelEnabled(logrus.DebugLevel))
if len(ac.Proxy) > 0 {
restyC.SetProxy(ac.Proxy)
}
if len(ac.Cert) > 0 {
restyC.SetRootCertificate(ac.Cert)
}
var MeshSysExe string
if len(ac.CustomMeshDir) > 0 {
MeshSysExe = filepath.Join(ac.CustomMeshDir, "MeshAgent.exe")
} else {
MeshSysExe = filepath.Join(os.Getenv("ProgramFiles"), "Mesh Agent", "MeshAgent.exe")
}
if runtime.GOOS == "linux" {
MeshSysExe = "/opt/tacticalmesh/meshagent"
}
svcConf := &service.Config{
Executable: exe,
Name: winSvcName,
DisplayName: "TacticalRMM Agent Service",
Arguments: []string{"-m", "svc"},
Description: "TacticalRMM Agent Service",
Option: service.KeyValue{
"StartType": "automatic",
"OnFailure": "restart",
"OnFailureDelayDuration": "5s",
"OnFailureResetPeriod": 10,
},
}
return &Agent{
Hostname: info.Hostname,
Arch: info.Architecture,
BaseURL: ac.BaseURL,
AgentID: ac.AgentID,
ApiURL: ac.APIURL,
Token: ac.Token,
AgentPK: ac.PK,
Cert: ac.Cert,
ProgramDir: pd,
EXE: exe,
SystemDrive: sd,
MeshInstaller: "meshagent.exe",
MeshSystemEXE: MeshSysExe,
MeshSVC: meshSvcName,
PyBin: pybin,
Headers: headers,
Logger: logger,
Version: version,
Debug: logger.IsLevelEnabled(logrus.DebugLevel),
rClient: restyC,
Proxy: ac.Proxy,
Platform: runtime.GOOS,
GoArch: runtime.GOARCH,
ServiceConfig: svcConf,
}
}
type CmdStatus struct {
Status gocmd.Status
Stdout string
Stderr string
}
type CmdOptions struct {
Shell string
Command string
Args []string
Timeout time.Duration
IsScript bool
IsExecutable bool
Detached bool
}
func (a *Agent) NewCMDOpts() *CmdOptions {
return &CmdOptions{
Shell: "/bin/bash",
Timeout: 30,
}
}
func (a *Agent) CmdV2(c *CmdOptions) CmdStatus {
ctx, cancel := context.WithTimeout(context.Background(), c.Timeout*time.Second)
defer cancel()
// Disable output buffering, enable streaming
cmdOptions := gocmd.Options{
Buffered: false,
Streaming: true,
}
// have a child process that is in a different process group so that
// parent terminating doesn't kill child
if c.Detached {
cmdOptions.BeforeExec = []func(cmd *exec.Cmd){
func(cmd *exec.Cmd) {
cmd.SysProcAttr = SetDetached()
},
}
}
var envCmd *gocmd.Cmd
if c.IsScript {
envCmd = gocmd.NewCmdOptions(cmdOptions, c.Shell, c.Args...) // call script directly
} else if c.IsExecutable {
envCmd = gocmd.NewCmdOptions(cmdOptions, c.Shell, c.Command) // c.Shell: bin + c.Command: args as one string
} else {
envCmd = gocmd.NewCmdOptions(cmdOptions, c.Shell, "-c", c.Command) // /bin/bash -c 'ls -l /var/log/...'
}
var stdoutBuf bytes.Buffer
var stderrBuf bytes.Buffer
// Print STDOUT and STDERR lines streaming from Cmd
doneChan := make(chan struct{})
go func() {
defer close(doneChan)
// Done when both channels have been closed
// https://dave.cheney.net/2013/04/30/curious-channels
for envCmd.Stdout != nil || envCmd.Stderr != nil {
select {
case line, open := <-envCmd.Stdout:
if !open {
envCmd.Stdout = nil
continue
}
fmt.Fprintln(&stdoutBuf, line)
a.Logger.Debugln(line)
case line, open := <-envCmd.Stderr:
if !open {
envCmd.Stderr = nil
continue
}
fmt.Fprintln(&stderrBuf, line)
a.Logger.Debugln(line)
}
}
}()
// Run and wait for Cmd to return, discard Status
envCmd.Start()
go func() {
select {
case <-doneChan:
return
case <-ctx.Done():
a.Logger.Debugf("Command timed out after %d seconds\n", c.Timeout)
pid := envCmd.Status().PID
a.Logger.Debugln("Killing process with PID", pid)
KillProc(int32(pid))
}
}()
// Wait for goroutine to print everything
<-doneChan
ret := CmdStatus{
Status: envCmd.Status(),
Stdout: CleanString(stdoutBuf.String()),
Stderr: CleanString(stderrBuf.String()),
}
a.Logger.Debugf("%+v\n", ret)
return ret
}
func (a *Agent) GetCPULoadAvg() int {
fallback := false
pyCode := `
import psutil
try:
print(int(round(psutil.cpu_percent(interval=10))), end='')
except:
print("pyerror", end='')
`
pypercent, err := a.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 {
a.Logger.Debugln("Go CPU Check:", err)
return 0
}
return int(math.Round(percent[0]))
}
return i
}
// ForceKillMesh kills all mesh agent related processes
func (a *Agent) ForceKillMesh() {
pids := make([]int, 0)
procs, err := ps.Processes()
if err != nil {
return
}
for _, process := range procs {
p, err := process.Info()
if err != nil {
continue
}
if strings.Contains(strings.ToLower(p.Name), "meshagent") {
pids = append(pids, p.PID)
}
}
for _, pid := range pids {
a.Logger.Debugln("Killing mesh process with pid %d", pid)
if err := KillProc(int32(pid)); err != nil {
a.Logger.Debugln(err)
}
}
}
func (a *Agent) SyncMeshNodeID() {
id, err := a.getMeshNodeID()
if err != nil {
a.Logger.Errorln("SyncMeshNodeID() getMeshNodeID()", err)
return
}
payload := rmm.MeshNodeID{
Func: "syncmesh",
Agentid: a.AgentID,
NodeID: StripAll(id),
}
_, err = a.rClient.R().SetBody(payload).Post("/api/v3/syncmesh/")
if err != nil {
a.Logger.Debugln("SyncMesh:", err)
}
}
func (a *Agent) setupNatsOptions() []nats.Option {
opts := make([]nats.Option, 0)
opts = append(opts, nats.Name("TacticalRMM"))
opts = append(opts, nats.UserInfo(a.AgentID, a.Token))
opts = append(opts, nats.ReconnectWait(time.Second*5))
opts = append(opts, nats.RetryOnFailedConnect(true))
opts = append(opts, nats.MaxReconnects(-1))
opts = append(opts, nats.ReconnectBufSize(-1))
return opts
}
func (a *Agent) GetUninstallExe() string {
cderr := os.Chdir(a.ProgramDir)
if cderr == nil {
files, err := filepath.Glob("unins*.exe")
if err == nil {
for _, f := range files {
if strings.Contains(f, "001") {
return f
}
}
}
}
return "unins000.exe"
}
func (a *Agent) CleanupAgentUpdates() {
cderr := os.Chdir(a.ProgramDir)
if cderr != nil {
a.Logger.Errorln(cderr)
return
}
files, err := filepath.Glob("winagent-v*.exe")
if err == nil {
for _, f := range files {
os.Remove(f)
}
}
cderr = os.Chdir(os.Getenv("TMP"))
if cderr != nil {
a.Logger.Errorln(cderr)
return
}
folders, err := filepath.Glob("tacticalrmm*")
if err == nil {
for _, f := range folders {
os.RemoveAll(f)
}
}
}
func (a *Agent) RunPythonCode(code string, timeout int, args []string) (string, error) {
content := []byte(code)
dir, err := ioutil.TempDir("", "tacticalpy")
if err != nil {
a.Logger.Debugln(err)
return "", err
}
defer os.RemoveAll(dir)
tmpfn, _ := ioutil.TempFile(dir, "*.py")
if _, err := tmpfn.Write(content); err != nil {
a.Logger.Debugln(err)
return "", err
}
if err := tmpfn.Close(); err != nil {
a.Logger.Debugln(err)
return "", err
}
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeout)*time.Second)
defer cancel()
var outb, errb bytes.Buffer
cmdArgs := []string{tmpfn.Name()}
if len(args) > 0 {
cmdArgs = append(cmdArgs, args...)
}
a.Logger.Debugln(cmdArgs)
cmd := exec.CommandContext(ctx, a.PyBin, cmdArgs...)
cmd.Stdout = &outb
cmd.Stderr = &errb
cmdErr := cmd.Run()
if ctx.Err() == context.DeadlineExceeded {
a.Logger.Debugln("RunPythonCode:", ctx.Err())
return "", ctx.Err()
}
if cmdErr != nil {
a.Logger.Debugln("RunPythonCode:", cmdErr)
return "", cmdErr
}
if errb.String() != "" {
a.Logger.Debugln(errb.String())
return errb.String(), errors.New("RunPythonCode stderr")
}
return outb.String(), nil
}
func (a *Agent) CreateTRMMTempDir() {
// create the temp dir for running scripts
dir := filepath.Join(os.TempDir(), "trmm")
if !trmm.FileExists(dir) {
err := os.Mkdir(dir, 0775)
if err != nil {
a.Logger.Errorln(err)
}
}
}

462
agent/agent_linux.go Normal file
View file

@ -0,0 +1,462 @@
/*
Copyright 2022 AmidaWare LLC.
Licensed under the Tactical RMM License Version 1.0 (the License).
You may only use the Licensed Software in accordance with the License.
A copy of the License is available at:
https://license.tacticalrmm.com
*/
package agent
import (
"bufio"
"fmt"
"os"
"runtime"
"strconv"
"strings"
"syscall"
"time"
rmm "github.com/amidaware/rmmagent/shared"
ps "github.com/elastic/go-sysinfo"
"github.com/go-resty/resty/v2"
"github.com/jaypipes/ghw"
"github.com/kardianos/service"
"github.com/shirou/gopsutil/v3/cpu"
"github.com/shirou/gopsutil/v3/disk"
psHost "github.com/shirou/gopsutil/v3/host"
"github.com/spf13/viper"
trmm "github.com/wh1te909/trmm-shared"
)
func ShowStatus(version string) {
fmt.Println(version)
}
func (a *Agent) GetDisks() []trmm.Disk {
ret := make([]trmm.Disk, 0)
partitions, err := disk.Partitions(false)
if err != nil {
a.Logger.Debugln(err)
return ret
}
for _, p := range partitions {
if strings.Contains(p.Device, "dev/loop") {
continue
}
usage, err := disk.Usage(p.Mountpoint)
if err != nil {
a.Logger.Debugln(err)
continue
}
d := trmm.Disk{
Device: p.Device,
Fstype: p.Fstype,
Total: ByteCountSI(usage.Total),
Used: ByteCountSI(usage.Used),
Free: ByteCountSI(usage.Free),
Percent: int(usage.UsedPercent),
}
ret = append(ret, d)
}
return ret
}
func (a *Agent) SystemRebootRequired() (bool, error) {
paths := [2]string{"/var/run/reboot-required", "/usr/bin/needs-restarting"}
for _, p := range paths {
if trmm.FileExists(p) {
return true, nil
}
}
return false, nil
}
func (a *Agent) LoggedOnUser() string {
var ret string
users, err := psHost.Users()
if err != nil {
return ret
}
// return the first logged in user
for _, user := range users {
if user.User != "" {
ret = user.User
break
}
}
return ret
}
func (a *Agent) osString() string {
h, err := psHost.Info()
if err != nil {
return "error getting host info"
}
return fmt.Sprintf("%s %s %s %s", strings.Title(h.Platform), h.PlatformVersion, h.KernelArch, h.KernelVersion)
}
func NewAgentConfig() *rmm.AgentConfig {
viper.SetConfigName("tacticalagent")
viper.SetConfigType("json")
viper.AddConfigPath("/etc/")
viper.AddConfigPath(".")
err := viper.ReadInConfig()
if err != nil {
return &rmm.AgentConfig{}
}
agentpk := viper.GetString("agentpk")
pk, _ := strconv.Atoi(agentpk)
ret := &rmm.AgentConfig{
BaseURL: viper.GetString("baseurl"),
AgentID: viper.GetString("agentid"),
APIURL: viper.GetString("apiurl"),
Token: viper.GetString("token"),
AgentPK: agentpk,
PK: pk,
Cert: viper.GetString("cert"),
Proxy: viper.GetString("proxy"),
CustomMeshDir: viper.GetString("meshdir"),
}
return ret
}
func (a *Agent) RunScript(code string, shell string, args []string, timeout int) (stdout, stderr string, exitcode int, e error) {
content := []byte(code)
f, err := os.CreateTemp("", "trmm")
if err != nil {
a.Logger.Errorln(err)
return "", err.Error(), 85, err
}
defer os.Remove(f.Name())
if _, err := f.Write(content); err != nil {
a.Logger.Errorln(err)
return "", err.Error(), 85, err
}
if err := f.Close(); err != nil {
a.Logger.Errorln(err)
return "", err.Error(), 85, err
}
if err := os.Chmod(f.Name(), 0770); err != nil {
a.Logger.Errorln(err)
return "", err.Error(), 85, err
}
opts := a.NewCMDOpts()
opts.IsScript = true
opts.Shell = f.Name()
opts.Args = args
opts.Timeout = time.Duration(timeout)
out := a.CmdV2(opts)
retError := ""
if out.Status.Error != nil {
retError += CleanString(out.Status.Error.Error())
retError += "\n"
}
if len(out.Stderr) > 0 {
retError += out.Stderr
}
return out.Stdout, retError, out.Status.Exit, nil
}
func SetDetached() *syscall.SysProcAttr {
return &syscall.SysProcAttr{Setpgid: true}
}
func (a *Agent) AgentUpdate(url, inno, version string) {
self, err := os.Executable()
if err != nil {
a.Logger.Errorln("AgentUpdate() os.Executable():", err)
return
}
f, err := os.CreateTemp("", "")
if err != nil {
a.Logger.Errorln("AgentUpdate()", err)
return
}
defer os.Remove(f.Name())
a.Logger.Infof("Agent updating from %s to %s", a.Version, version)
a.Logger.Infoln("Downloading agent update from", url)
rClient := resty.New()
rClient.SetCloseConnection(true)
rClient.SetTimeout(15 * time.Minute)
rClient.SetDebug(a.Debug)
if len(a.Proxy) > 0 {
rClient.SetProxy(a.Proxy)
}
r, err := rClient.R().SetOutput(f.Name()).Get(url)
if err != nil {
a.Logger.Errorln("AgentUpdate() download:", err)
f.Close()
return
}
if r.IsError() {
a.Logger.Errorln("AgentUpdate() status code:", r.StatusCode())
f.Close()
return
}
f.Close()
os.Chmod(f.Name(), 0755)
err = os.Rename(f.Name(), self)
if err != nil {
a.Logger.Errorln("AgentUpdate() os.Rename():", err)
return
}
opts := a.NewCMDOpts()
opts.Detached = true
opts.Command = "systemctl restart tacticalagent.service"
a.CmdV2(opts)
}
func (a *Agent) AgentUninstall(code string) {
f, err := os.CreateTemp("", "trmm")
if err != nil {
a.Logger.Errorln("AgentUninstall CreateTemp:", err)
return
}
f.Write([]byte(code))
f.Close()
os.Chmod(f.Name(), 0770)
opts := a.NewCMDOpts()
opts.IsScript = true
opts.Shell = f.Name()
opts.Args = []string{"uninstall"}
opts.Detached = true
a.CmdV2(opts)
}
func (a *Agent) NixMeshNodeID() string {
var meshNodeID string
meshSuccess := false
a.Logger.Debugln("Getting mesh node id")
opts := a.NewCMDOpts()
opts.IsExecutable = true
opts.Shell = a.MeshSystemEXE
opts.Command = "-nodeid"
for !meshSuccess {
out := a.CmdV2(opts)
meshNodeID = out.Stdout
if meshNodeID == "" {
time.Sleep(1 * time.Second)
continue
} else if strings.Contains(strings.ToLower(meshNodeID), "graphical version") || strings.Contains(strings.ToLower(meshNodeID), "zenity") {
a.Logger.Debugln(out.Stdout)
time.Sleep(1 * time.Second)
continue
}
meshSuccess = true
}
return meshNodeID
}
func (a *Agent) getMeshNodeID() (string, error) {
return a.NixMeshNodeID(), nil
}
func (a *Agent) RecoverMesh() {
a.Logger.Infoln("Attempting mesh recovery")
opts := a.NewCMDOpts()
opts.Command = "systemctl restart meshagent.service"
a.CmdV2(opts)
a.SyncMeshNodeID()
}
func (a *Agent) GetWMIInfo() map[string]interface{} {
wmiInfo := make(map[string]interface{})
ips := make([]string, 0)
disks := make([]string, 0)
cpus := make([]string, 0)
gpus := make([]string, 0)
// local ips
host, err := ps.Host()
if err != nil {
a.Logger.Errorln("GetWMIInfo() ps.Host()", err)
} else {
for _, ip := range host.Info().IPs {
if strings.Contains(ip, "127.0.") || strings.Contains(ip, "::1/128") {
continue
}
ips = append(ips, ip)
}
}
wmiInfo["local_ips"] = ips
// disks
block, err := ghw.Block(ghw.WithDisableWarnings())
if err != nil {
a.Logger.Errorln("ghw.Block()", err)
} else {
for _, disk := range block.Disks {
if disk.IsRemovable || strings.Contains(disk.Name, "ram") {
continue
}
ret := fmt.Sprintf("%s %s %s %s %s %s", disk.Vendor, disk.Model, disk.StorageController, disk.DriveType, disk.Name, ByteCountSI(disk.SizeBytes))
ret = strings.TrimSpace(strings.ReplaceAll(ret, "unknown", ""))
disks = append(disks, ret)
}
}
wmiInfo["disks"] = disks
// cpus
cpuInfo, err := cpu.Info()
if err != nil {
a.Logger.Errorln("cpu.Info()", err)
} else {
if len(cpuInfo) > 0 {
if cpuInfo[0].ModelName != "" {
cpus = append(cpus, cpuInfo[0].ModelName)
}
}
}
wmiInfo["cpus"] = cpus
// make/model
wmiInfo["make_model"] = ""
chassis, err := ghw.Chassis(ghw.WithDisableWarnings())
if err != nil {
a.Logger.Errorln("ghw.Chassis()", err)
} else {
if chassis.Vendor != "" || chassis.Version != "" {
wmiInfo["make_model"] = fmt.Sprintf("%s %s", chassis.Vendor, chassis.Version)
}
}
// gfx cards
gpu, err := ghw.GPU(ghw.WithDisableWarnings())
if err != nil {
a.Logger.Errorln("ghw.GPU()", err)
} else {
for _, i := range gpu.GraphicsCards {
if i.DeviceInfo != nil {
ret := fmt.Sprintf("%s %s", i.DeviceInfo.Vendor.Name, i.DeviceInfo.Product.Name)
gpus = append(gpus, ret)
}
}
}
wmiInfo["gpus"] = gpus
// temp hack for ARM cpu/make/model if rasp pi
var makeModel string
if strings.Contains(runtime.GOARCH, "arm") {
file, _ := os.Open("/proc/cpuinfo")
scanner := bufio.NewScanner(file)
for scanner.Scan() {
if strings.Contains(strings.ToLower(scanner.Text()), "raspberry") {
model := strings.Split(scanner.Text(), ":")
if len(model) == 2 {
makeModel = strings.TrimSpace(model[1])
break
}
}
}
}
if len(cpus) == 0 {
wmiInfo["cpus"] = []string{makeModel}
}
if makeModel != "" && (wmiInfo["make_model"] == "" || wmiInfo["make_model"] == "unknown unknown") {
wmiInfo["make_model"] = makeModel
}
return wmiInfo
}
// windows only below TODO add into stub file
func (a *Agent) PlatVer() (string, error) { return "", nil }
func (a *Agent) SendSoftware() {}
func (a *Agent) UninstallCleanup() {}
func (a *Agent) RunMigrations() {}
func GetServiceStatus(name string) (string, error) { return "", nil }
func (a *Agent) GetPython(force bool) {}
type SchedTask struct{ Name string }
func (a *Agent) PatchMgmnt(enable bool) error { return nil }
func (a *Agent) CreateSchedTask(st SchedTask) (bool, error) { return false, nil }
func DeleteSchedTask(name string) error { return nil }
func ListSchedTasks() []string { return []string{} }
func (a *Agent) GetEventLog(logName string, searchLastDays int) []rmm.EventLogMsg {
return []rmm.EventLogMsg{}
}
func (a *Agent) GetServiceDetail(name string) trmm.WindowsService { return trmm.WindowsService{} }
func (a *Agent) ControlService(name, action string) rmm.WinSvcResp {
return rmm.WinSvcResp{Success: false, ErrorMsg: "/na"}
}
func (a *Agent) EditService(name, startupType string) rmm.WinSvcResp {
return rmm.WinSvcResp{Success: false, ErrorMsg: "/na"}
}
func (a *Agent) GetInstalledSoftware() []trmm.WinSoftwareList { return []trmm.WinSoftwareList{} }
func (a *Agent) ChecksRunning() bool { return false }
func (a *Agent) RunTask(id int) error { return nil }
func (a *Agent) InstallChoco() {}
func (a *Agent) InstallWithChoco(name string) (string, error) { return "", nil }
func (a *Agent) GetWinUpdates() {}
func (a *Agent) InstallUpdates(guids []string) {}
func (a *Agent) installMesh(meshbin, exe, proxy string) (string, error) {
return "not implemented", nil
}
func CMDShell(shell string, cmdArgs []string, command string, timeout int, detached bool) (output [2]string, e error) {
return [2]string{"", ""}, nil
}
func CMD(exe string, args []string, timeout int, detached bool) (output [2]string, e error) {
return [2]string{"", ""}, nil
}
func (a *Agent) GetServices() []trmm.WindowsService { return []trmm.WindowsService{} }
func (a *Agent) Start(_ service.Service) error { return nil }
func (a *Agent) Stop(_ service.Service) error { return nil }
func (a *Agent) InstallService() error { return nil }

853
agent/agent_windows.go Normal file
View file

@ -0,0 +1,853 @@
/*
Copyright 2022 AmidaWare LLC.
Licensed under the Tactical RMM License Version 1.0 (the License).
You may only use the Licensed Software in accordance with the License.
A copy of the License is available at:
https://license.tacticalrmm.com
*/
package agent
import (
"bytes"
"context"
"errors"
"fmt"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"runtime"
"strconv"
"strings"
"sync"
"syscall"
"time"
"unsafe"
rmm "github.com/amidaware/rmmagent/shared"
ps "github.com/elastic/go-sysinfo"
"github.com/go-ole/go-ole"
"github.com/go-ole/go-ole/oleutil"
"github.com/go-resty/resty/v2"
"github.com/gonutz/w32/v2"
"github.com/kardianos/service"
"github.com/shirou/gopsutil/v3/disk"
wapf "github.com/wh1te909/go-win64api"
trmm "github.com/wh1te909/trmm-shared"
"golang.org/x/sys/windows"
"golang.org/x/sys/windows/registry"
)
var (
getDriveType = windows.NewLazySystemDLL("kernel32.dll").NewProc("GetDriveTypeW")
)
func NewAgentConfig() *rmm.AgentConfig {
k, err := registry.OpenKey(registry.LOCAL_MACHINE, `SOFTWARE\TacticalRMM`, registry.ALL_ACCESS)
if err != nil {
return &rmm.AgentConfig{}
}
baseurl, _, _ := k.GetStringValue("BaseURL")
agentid, _, _ := k.GetStringValue("AgentID")
apiurl, _, _ := k.GetStringValue("ApiURL")
token, _, _ := k.GetStringValue("Token")
agentpk, _, _ := k.GetStringValue("AgentPK")
pk, _ := strconv.Atoi(agentpk)
cert, _, _ := k.GetStringValue("Cert")
proxy, _, _ := k.GetStringValue("Proxy")
customMeshDir, _, _ := k.GetStringValue("MeshDir")
return &rmm.AgentConfig{
BaseURL: baseurl,
AgentID: agentid,
APIURL: apiurl,
Token: token,
AgentPK: agentpk,
PK: pk,
Cert: cert,
Proxy: proxy,
CustomMeshDir: customMeshDir,
}
}
func (a *Agent) 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")
if !trmm.FileExists(dir) {
a.CreateTRMMTempDir()
}
const defaultExitCode = 1
var (
outb bytes.Buffer
errb bytes.Buffer
exe string
ext string
cmdArgs []string
)
switch shell {
case "powershell":
ext = "*.ps1"
case "python":
ext = "*.py"
case "cmd":
ext = "*.bat"
}
tmpfn, err := ioutil.TempFile(dir, ext)
if err != nil {
a.Logger.Errorln(err)
return "", err.Error(), 85, err
}
defer os.Remove(tmpfn.Name())
if _, err := tmpfn.Write(content); err != nil {
a.Logger.Errorln(err)
return "", err.Error(), 85, err
}
if err := tmpfn.Close(); err != nil {
a.Logger.Errorln(err)
return "", err.Error(), 85, err
}
switch shell {
case "powershell":
exe = "Powershell"
cmdArgs = []string{"-NonInteractive", "-NoProfile", "-ExecutionPolicy", "Bypass", tmpfn.Name()}
case "python":
exe = a.PyBin
cmdArgs = []string{tmpfn.Name()}
case "cmd":
exe = tmpfn.Name()
}
if len(args) > 0 {
cmdArgs = append(cmdArgs, args...)
}
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeout)*time.Second)
defer cancel()
var timedOut bool = false
cmd := exec.Command(exe, cmdArgs...)
cmd.Stdout = &outb
cmd.Stderr = &errb
if cmdErr := cmd.Start(); cmdErr != nil {
a.Logger.Debugln(cmdErr)
return "", cmdErr.Error(), 65, cmdErr
}
pid := int32(cmd.Process.Pid)
// custom context handling, we need to kill child procs if this is a batch script,
// otherwise it will hang forever
// the normal exec.CommandContext() doesn't work since it only kills the parent process
go func(p int32) {
<-ctx.Done()
_ = KillProc(p)
timedOut = true
}(pid)
cmdErr := cmd.Wait()
if timedOut {
stdout = CleanString(outb.String())
stderr = fmt.Sprintf("%s\nScript timed out after %d seconds", CleanString(errb.String()), timeout)
exitcode = 98
a.Logger.Debugln("Script check timeout:", ctx.Err())
} else {
stdout = CleanString(outb.String())
stderr = CleanString(errb.String())
// get the exit code
if cmdErr != nil {
if exitError, ok := cmdErr.(*exec.ExitError); ok {
if ws, ok := exitError.Sys().(syscall.WaitStatus); ok {
exitcode = ws.ExitStatus()
} else {
exitcode = defaultExitCode
}
} else {
exitcode = defaultExitCode
}
} else {
if ws, ok := cmd.ProcessState.Sys().(syscall.WaitStatus); ok {
exitcode = ws.ExitStatus()
} else {
exitcode = 0
}
}
}
return stdout, stderr, exitcode, nil
}
func SetDetached() *syscall.SysProcAttr {
return &syscall.SysProcAttr{
CreationFlags: windows.DETACHED_PROCESS | windows.CREATE_NEW_PROCESS_GROUP,
}
}
func CMD(exe string, args []string, timeout int, detached bool) (output [2]string, e error) {
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeout)*time.Second)
defer cancel()
var outb, errb bytes.Buffer
cmd := exec.CommandContext(ctx, exe, args...)
if detached {
cmd.SysProcAttr = &windows.SysProcAttr{
CreationFlags: windows.DETACHED_PROCESS | windows.CREATE_NEW_PROCESS_GROUP,
}
}
cmd.Stdout = &outb
cmd.Stderr = &errb
err := cmd.Run()
if err != nil {
return [2]string{"", ""}, fmt.Errorf("%s: %s", err, CleanString(errb.String()))
}
if ctx.Err() == context.DeadlineExceeded {
return [2]string{"", ""}, ctx.Err()
}
return [2]string{CleanString(outb.String()), CleanString(errb.String())}, nil
}
func CMDShell(shell string, cmdArgs []string, command string, timeout int, detached bool) (output [2]string, e error) {
var (
outb bytes.Buffer
errb bytes.Buffer
cmd *exec.Cmd
timedOut = false
)
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeout)*time.Second)
defer cancel()
if len(cmdArgs) > 0 && command == "" {
switch shell {
case "cmd":
cmdArgs = append([]string{"/C"}, cmdArgs...)
cmd = exec.Command("cmd.exe", cmdArgs...)
case "powershell":
cmdArgs = append([]string{"-NonInteractive", "-NoProfile"}, cmdArgs...)
cmd = exec.Command("powershell.exe", cmdArgs...)
}
} else {
switch shell {
case "cmd":
cmd = exec.Command("cmd.exe")
cmd.SysProcAttr = &windows.SysProcAttr{
CmdLine: fmt.Sprintf("cmd.exe /C %s", command),
}
case "powershell":
cmd = exec.Command("Powershell", "-NonInteractive", "-NoProfile", command)
}
}
// https://docs.microsoft.com/en-us/windows/win32/procthread/process-creation-flags
if detached {
cmd.SysProcAttr = &windows.SysProcAttr{
CreationFlags: windows.DETACHED_PROCESS | windows.CREATE_NEW_PROCESS_GROUP,
}
}
cmd.Stdout = &outb
cmd.Stderr = &errb
cmd.Start()
pid := int32(cmd.Process.Pid)
go func(p int32) {
<-ctx.Done()
_ = KillProc(p)
timedOut = true
}(pid)
err := cmd.Wait()
if timedOut {
return [2]string{CleanString(outb.String()), CleanString(errb.String())}, ctx.Err()
}
if err != nil {
return [2]string{CleanString(outb.String()), CleanString(errb.String())}, err
}
return [2]string{CleanString(outb.String()), CleanString(errb.String())}, nil
}
// GetDisks returns a list of fixed disks
func (a *Agent) GetDisks() []trmm.Disk {
ret := make([]trmm.Disk, 0)
partitions, err := disk.Partitions(false)
if err != nil {
a.Logger.Debugln(err)
return ret
}
for _, p := range partitions {
typepath, _ := windows.UTF16PtrFromString(p.Device)
typeval, _, _ := getDriveType.Call(uintptr(unsafe.Pointer(typepath)))
// https://docs.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-getdrivetypea
if typeval != 3 {
continue
}
usage, err := disk.Usage(p.Mountpoint)
if err != nil {
a.Logger.Debugln(err)
continue
}
d := trmm.Disk{
Device: p.Device,
Fstype: p.Fstype,
Total: ByteCountSI(usage.Total),
Used: ByteCountSI(usage.Used),
Free: ByteCountSI(usage.Free),
Percent: int(usage.UsedPercent),
}
ret = append(ret, d)
}
return ret
}
// LoggedOnUser returns the first logged on user it finds
func (a *Agent) LoggedOnUser() string {
pyCode := `
import psutil
try:
u = psutil.users()[0].name
if u.isascii():
print(u, end='')
else:
print('notascii', end='')
except Exception as e:
print("None", end='')
`
// try with psutil first, if fails, fallback to golang
user, err := a.RunPythonCode(pyCode, 5, []string{})
if err == nil && user != "notascii" {
return user
}
users, err := wapf.ListLoggedInUsers()
if err != nil {
a.Logger.Debugln("LoggedOnUser error", err)
return "None"
}
if len(users) == 0 {
return "None"
}
for _, u := range users {
// remove the computername or domain
return strings.Split(u.FullUser(), `\`)[1]
}
return "None"
}
// ShowStatus prints windows service status
// If called from an interactive desktop, pops up a message box
// Otherwise prints to the console
func ShowStatus(version string) {
statusMap := make(map[string]string)
svcs := []string{winSvcName, meshSvcName}
for _, service := range svcs {
status, err := GetServiceStatus(service)
if err != nil {
statusMap[service] = "Not Installed"
continue
}
statusMap[service] = status
}
window := w32.GetForegroundWindow()
if window != 0 {
_, consoleProcID := w32.GetWindowThreadProcessId(window)
if w32.GetCurrentProcessId() == consoleProcID {
w32.ShowWindow(window, w32.SW_HIDE)
}
var handle w32.HWND
msg := fmt.Sprintf("Agent: %s\n\nMesh Agent: %s", statusMap[winSvcName], statusMap[meshSvcName])
w32.MessageBox(handle, msg, fmt.Sprintf("Tactical RMM v%s", version), w32.MB_OK|w32.MB_ICONINFORMATION)
} else {
fmt.Println("Tactical RMM Version", version)
fmt.Println("Tactical Agent:", statusMap[winSvcName])
fmt.Println("Mesh Agent:", statusMap[meshSvcName])
}
}
// PatchMgmnt enables/disables automatic update
// 0 - Enable Automatic Updates (Default)
// 1 - Disable Automatic Updates
// https://docs.microsoft.com/en-us/previous-versions/windows/it-pro/windows-server-2008-R2-and-2008/dd939844(v=ws.10)?redirectedfrom=MSDN
func (a *Agent) PatchMgmnt(enable bool) error {
var val uint32
k, _, err := registry.CreateKey(registry.LOCAL_MACHINE, `SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU`, registry.ALL_ACCESS)
if err != nil {
return err
}
if enable {
val = 1
} else {
val = 0
}
err = k.SetDWordValue("AUOptions", val)
if err != nil {
return err
}
return nil
}
func (a *Agent) PlatVer() (string, error) {
k, err := registry.OpenKey(registry.LOCAL_MACHINE, `SOFTWARE\Microsoft\Windows NT\CurrentVersion`, registry.ALL_ACCESS)
if err != nil {
return "n/a", err
}
defer k.Close()
dv, _, err := k.GetStringValue("DisplayVersion")
if err == nil {
return dv, nil
}
relid, _, err := k.GetStringValue("ReleaseId")
if err != nil {
return "n/a", err
}
return relid, nil
}
// EnablePing enables ping
func EnablePing() {
args := make([]string, 0)
cmd := `netsh advfirewall firewall add rule name="ICMP Allow incoming V4 echo request" protocol=icmpv4:8,any dir=in action=allow`
_, err := CMDShell("cmd", args, cmd, 10, false)
if err != nil {
fmt.Println(err)
}
}
// EnableRDP enables Remote Desktop
func EnableRDP() {
k, _, err := registry.CreateKey(registry.LOCAL_MACHINE, `SYSTEM\CurrentControlSet\Control\Terminal Server`, registry.ALL_ACCESS)
if err != nil {
fmt.Println(err)
}
defer k.Close()
err = k.SetDWordValue("fDenyTSConnections", 0)
if err != nil {
fmt.Println(err)
}
args := make([]string, 0)
cmd := `netsh advfirewall firewall set rule group="remote desktop" new enable=Yes`
_, cerr := CMDShell("cmd", args, cmd, 10, false)
if cerr != nil {
fmt.Println(cerr)
}
}
// DisableSleepHibernate disables sleep and hibernate
func DisableSleepHibernate() {
k, _, err := registry.CreateKey(registry.LOCAL_MACHINE, `SYSTEM\CurrentControlSet\Control\Session Manager\Power`, registry.ALL_ACCESS)
if err != nil {
fmt.Println(err)
}
defer k.Close()
err = k.SetDWordValue("HiberbootEnabled", 0)
if err != nil {
fmt.Println(err)
}
args := make([]string, 0)
var wg sync.WaitGroup
currents := []string{"ac", "dc"}
for _, i := range currents {
wg.Add(1)
go func(c string) {
defer wg.Done()
_, _ = CMDShell("cmd", args, fmt.Sprintf("powercfg /set%svalueindex scheme_current sub_buttons lidaction 0", c), 5, false)
_, _ = CMDShell("cmd", args, fmt.Sprintf("powercfg /x -standby-timeout-%s 0", c), 5, false)
_, _ = CMDShell("cmd", args, fmt.Sprintf("powercfg /x -hibernate-timeout-%s 0", c), 5, false)
_, _ = CMDShell("cmd", args, fmt.Sprintf("powercfg /x -disk-timeout-%s 0", c), 5, false)
_, _ = CMDShell("cmd", args, fmt.Sprintf("powercfg /x -monitor-timeout-%s 0", c), 5, false)
}(i)
}
wg.Wait()
_, _ = CMDShell("cmd", args, "powercfg -S SCHEME_CURRENT", 5, false)
}
// NewCOMObject creates a new COM object for the specifed ProgramID.
func NewCOMObject(id string) (*ole.IDispatch, error) {
unknown, err := oleutil.CreateObject(id)
if err != nil {
return nil, fmt.Errorf("unable to create initial unknown object: %v", err)
}
defer unknown.Release()
obj, err := unknown.QueryInterface(ole.IID_IDispatch)
if err != nil {
return nil, fmt.Errorf("unable to create query interface: %v", err)
}
return obj, nil
}
// SystemRebootRequired checks whether a system reboot is required.
func (a *Agent) SystemRebootRequired() (bool, error) {
regKeys := []string{
`SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\RebootRequired`,
}
for _, key := range regKeys {
k, err := registry.OpenKey(registry.LOCAL_MACHINE, key, registry.QUERY_VALUE)
if err == nil {
k.Close()
return true, nil
} else if err != registry.ErrNotExist {
return false, err
}
}
return false, nil
}
func (a *Agent) SendSoftware() {
sw := a.GetInstalledSoftware()
a.Logger.Debugln(sw)
payload := map[string]interface{}{"agent_id": a.AgentID, "software": sw}
_, err := a.rClient.R().SetBody(payload).Post("/api/v3/software/")
if err != nil {
a.Logger.Debugln(err)
}
}
func (a *Agent) UninstallCleanup() {
registry.DeleteKey(registry.LOCAL_MACHINE, `SOFTWARE\TacticalRMM`)
a.PatchMgmnt(false)
a.CleanupAgentUpdates()
CleanupSchedTasks()
}
func (a *Agent) AgentUpdate(url, inno, version string) {
time.Sleep(time.Duration(randRange(1, 15)) * time.Second)
a.KillHungUpdates()
a.CleanupAgentUpdates()
updater := filepath.Join(a.ProgramDir, inno)
a.Logger.Infof("Agent updating from %s to %s", a.Version, version)
a.Logger.Infoln("Downloading agent update from", url)
rClient := resty.New()
rClient.SetCloseConnection(true)
rClient.SetTimeout(15 * time.Minute)
rClient.SetDebug(a.Debug)
if len(a.Proxy) > 0 {
rClient.SetProxy(a.Proxy)
}
r, err := rClient.R().SetOutput(updater).Get(url)
if err != nil {
a.Logger.Errorln(err)
CMD("net", []string{"start", winSvcName}, 10, false)
return
}
if r.IsError() {
a.Logger.Errorln("Download failed with status code", r.StatusCode())
CMD("net", []string{"start", winSvcName}, 10, false)
return
}
dir, err := ioutil.TempDir("", "tacticalrmm")
if err != nil {
a.Logger.Errorln("Agentupdate create tempdir:", err)
CMD("net", []string{"start", winSvcName}, 10, false)
return
}
innoLogFile := filepath.Join(dir, "tacticalrmm.txt")
args := []string{"/C", updater, "/VERYSILENT", fmt.Sprintf("/LOG=%s", innoLogFile)}
cmd := exec.Command("cmd.exe", args...)
cmd.SysProcAttr = &windows.SysProcAttr{
CreationFlags: windows.DETACHED_PROCESS | windows.CREATE_NEW_PROCESS_GROUP,
}
cmd.Start()
time.Sleep(1 * time.Second)
}
func (a *Agent) osString() string {
host, _ := ps.Host()
info := host.Info()
osInf := info.OS
var arch string
switch info.Architecture {
case "x86_64":
arch = "64 bit"
case "x86":
arch = "32 bit"
}
var osFullName string
platver, err := a.PlatVer()
if err != nil {
osFullName = fmt.Sprintf("%s, %s (build %s)", osInf.Name, arch, osInf.Build)
} else {
osFullName = fmt.Sprintf("%s, %s v%s (build %s)", osInf.Name, arch, platver, osInf.Build)
}
return osFullName
}
func (a *Agent) AgentUninstall(code string) {
a.KillHungUpdates()
tacUninst := filepath.Join(a.ProgramDir, a.GetUninstallExe())
args := []string{"/C", tacUninst, "/VERYSILENT"}
cmd := exec.Command("cmd.exe", args...)
cmd.SysProcAttr = &windows.SysProcAttr{
CreationFlags: windows.DETACHED_PROCESS | windows.CREATE_NEW_PROCESS_GROUP,
}
cmd.Start()
}
func (a *Agent) addDefenderExlusions() {
code := `
Add-MpPreference -ExclusionPath 'C:\Program Files\TacticalAgent\*'
Add-MpPreference -ExclusionPath 'C:\Windows\Temp\winagent-v*.exe'
Add-MpPreference -ExclusionPath 'C:\Windows\Temp\trmm\*'
Add-MpPreference -ExclusionPath 'C:\Program Files\Mesh Agent\*'
`
_, _, _, err := a.RunScript(code, "powershell", []string{}, 20)
if err != nil {
a.Logger.Debugln(err)
}
}
// RunMigrations cleans up unused stuff from older agents
func (a *Agent) RunMigrations() {
for _, i := range []string{"nssm.exe", "nssm-x86.exe"} {
nssm := filepath.Join(a.ProgramDir, i)
if trmm.FileExists(nssm) {
os.Remove(nssm)
}
}
}
func (a *Agent) 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 := 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 := 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 = 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
}
// 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 (a *Agent) 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 != a.EXE {
continue
}
for _, arg := range p.Args {
if arg == "runchecks" || arg == "checkrunner" {
running = true
break Out
}
}
}
return running
}
func (a *Agent) GetPython(force bool) {
if trmm.FileExists(a.PyBin) && !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(a.ProgramDir, folder)
pyZip := filepath.Join(a.ProgramDir, archZip)
a.Logger.Debugln(pyZip)
a.Logger.Debugln(a.PyBin)
defer os.Remove(pyZip)
if force {
os.RemoveAll(pyFolder)
}
rClient := resty.New()
rClient.SetTimeout(20 * time.Minute)
rClient.SetRetryCount(10)
rClient.SetRetryWaitTime(1 * time.Minute)
rClient.SetRetryMaxWaitTime(15 * time.Minute)
if len(a.Proxy) > 0 {
rClient.SetProxy(a.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 = Unzip(pyZip, a.ProgramDir)
if err != nil {
a.Logger.Errorln(err)
}
}
func (a *Agent) RecoverMesh() {
a.Logger.Infoln("Attempting mesh recovery")
defer CMD("net", []string{"start", a.MeshSVC}, 60, false)
_, _ = CMD("net", []string{"stop", a.MeshSVC}, 60, false)
a.ForceKillMesh()
a.SyncMeshNodeID()
}
func (a *Agent) getMeshNodeID() (string, error) {
out, err := CMD(a.MeshSystemEXE, []string{"-nodeid"}, 10, false)
if err != nil {
a.Logger.Debugln(err)
return "", err
}
stdout := out[0]
stderr := out[1]
if stderr != "" {
a.Logger.Debugln(stderr)
return "", err
}
if stdout == "" || strings.Contains(strings.ToLower(StripAll(stdout)), "not defined") {
a.Logger.Debugln("Failed getting mesh node id", stdout)
return "", errors.New("failed to get mesh node id")
}
return stdout, nil
}
func (a *Agent) Start(_ service.Service) error {
go a.RunRPC()
return nil
}
func (a *Agent) Stop(_ service.Service) error {
return nil
}
func (a *Agent) InstallService() error {
if serviceExists(winSvcName) {
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 := service.New(a, a.ServiceConfig)
if err != nil {
return err
}
return service.Control(s, "install")
}
// TODO add to stub
func (a *Agent) NixMeshNodeID() string {
return "not implemented"
}

93
agent/checkin.go Normal file
View file

@ -0,0 +1,93 @@
/*
Copyright 2022 AmidaWare LLC.
Licensed under the Tactical RMM License Version 1.0 (the License).
You may only use the Licensed Software in accordance with the License.
A copy of the License is available at:
https://license.tacticalrmm.com
*/
package agent
import (
"fmt"
"runtime"
"time"
nats "github.com/nats-io/nats.go"
"github.com/ugorji/go/codec"
trmm "github.com/wh1te909/trmm-shared"
)
func (a *Agent) NatsMessage(nc *nats.Conn, mode string) {
var resp []byte
var payload interface{}
ret := codec.NewEncoderBytes(&resp, new(codec.MsgpackHandle))
switch mode {
case "agent-hello":
payload = trmm.CheckInNats{
Agentid: a.AgentID,
Version: a.Version,
}
case "agent-winsvc":
payload = trmm.WinSvcNats{
Agentid: a.AgentID,
WinSvcs: a.GetServices(),
}
case "agent-agentinfo":
osinfo := a.osString()
reboot, err := a.SystemRebootRequired()
if err != nil {
reboot = false
}
payload = trmm.AgentInfoNats{
Agentid: a.AgentID,
Username: a.LoggedOnUser(),
Hostname: a.Hostname,
OS: osinfo,
Platform: runtime.GOOS,
TotalRAM: a.TotalRAM(),
BootTime: a.BootTime(),
RebootNeeded: reboot,
GoArch: a.GoArch,
}
case "agent-wmi":
payload = trmm.WinWMINats{
Agentid: a.AgentID,
WMI: a.GetWMIInfo(),
}
case "agent-disks":
payload = trmm.WinDisksNats{
Agentid: a.AgentID,
Disks: a.GetDisks(),
}
case "agent-publicip":
payload = trmm.PublicIPNats{
Agentid: a.AgentID,
PublicIP: a.PublicIP(),
}
}
a.Logger.Debugln(mode, payload)
ret.Encode(payload)
nc.PublishRequest(a.AgentID, mode, resp)
}
func (a *Agent) DoNatsCheckIn() {
opts := a.setupNatsOptions()
server := fmt.Sprintf("tls://%s:4222", a.ApiURL)
nc, err := nats.Connect(server, opts...)
if err != nil {
a.Logger.Errorln(err)
return
}
for _, s := range natsCheckin {
time.Sleep(time.Duration(randRange(100, 400)) * time.Millisecond)
a.NatsMessage(nc, s)
}
nc.Close()
}

379
agent/checks.go Normal file
View file

@ -0,0 +1,379 @@
/*
Copyright 2022 AmidaWare LLC.
Licensed under the Tactical RMM License Version 1.0 (the License).
You may only use the Licensed Software in accordance with the License.
A copy of the License is available at:
https://license.tacticalrmm.com
*/
package agent
import (
"encoding/json"
"fmt"
"math"
"runtime"
"strings"
"sync"
"time"
rmm "github.com/amidaware/rmmagent/shared"
ps "github.com/elastic/go-sysinfo"
"github.com/go-resty/resty/v2"
"github.com/shirou/gopsutil/v3/disk"
)
func (a *Agent) CheckRunner() {
sleepDelay := randRange(14, 22)
a.Logger.Debugf("CheckRunner() init sleeping for %v seconds", sleepDelay)
time.Sleep(time.Duration(sleepDelay) * time.Second)
for {
interval, err := a.GetCheckInterval()
if err == nil && !a.ChecksRunning() {
if runtime.GOOS == "windows" {
_, err = CMD(a.EXE, []string{"-m", "checkrunner"}, 600, false)
if err != nil {
a.Logger.Errorln("Checkrunner RunChecks", err)
}
} else {
a.RunChecks(false)
}
}
a.Logger.Debugln("Checkrunner sleeping for", interval)
time.Sleep(time.Duration(interval) * time.Second)
}
}
func (a *Agent) GetCheckInterval() (int, error) {
r, err := a.rClient.R().SetResult(&rmm.CheckInfo{}).Get(fmt.Sprintf("/api/v3/%s/checkinterval/", a.AgentID))
if err != nil {
a.Logger.Debugln(err)
return 120, err
}
if r.IsError() {
a.Logger.Debugln("Checkinterval response code:", r.StatusCode())
return 120, fmt.Errorf("checkinterval response code: %v", r.StatusCode())
}
interval := r.Result().(*rmm.CheckInfo).Interval
return interval, nil
}
func (a *Agent) RunChecks(force bool) error {
data := rmm.AllChecks{}
var url string
if force {
url = fmt.Sprintf("/api/v3/%s/runchecks/", a.AgentID)
} else {
url = fmt.Sprintf("/api/v3/%s/checkrunner/", a.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()
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
}
type ScriptCheckResult struct {
ID int `json:"id"`
Stdout string `json:"stdout"`
Stderr string `json:"stderr"`
Retcode int `json:"retcode"`
Runtime float64 `json:"runtime"`
}
// ScriptCheck runs either bat, powershell or python script
func (a *Agent) ScriptCheck(data rmm.Check, r *resty.Client) {
start := time.Now()
stdout, stderr, retcode, _ := a.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 := r.R().SetBody(payload).Patch("/api/v3/checkrunner/")
if err != nil {
a.Logger.Debugln(err)
}
}
func (a *Agent) SendDiskCheckResult(payload DiskCheckResult, r *resty.Client) {
_, err := r.R().SetBody(payload).Patch("/api/v3/checkrunner/")
if err != nil {
a.Logger.Debugln(err)
}
}
type DiskCheckResult struct {
ID int `json:"id"`
MoreInfo string `json:"more_info"`
PercentUsed float64 `json:"percent_used"`
Exists bool `json:"exists"`
}
// DiskCheck checks disk usage
func (a *Agent) DiskCheck(data rmm.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)
a.Logger.Debugln("Disk", data.Disk, err)
return
}
payload.Exists = true
payload.PercentUsed = usage.UsedPercent
payload.MoreInfo = fmt.Sprintf("Total: %s, Free: %s", ByteCountSI(usage.Total), ByteCountSI(usage.Free))
return
}
type CPUMemResult struct {
ID int `json:"id"`
Percent int `json:"percent"`
}
// CPULoadCheck checks avg cpu load
func (a *Agent) CPULoadCheck(data rmm.Check, r *resty.Client) {
payload := CPUMemResult{ID: data.CheckPK, Percent: a.GetCPULoadAvg()}
_, err := r.R().SetBody(payload).Patch("/api/v3/checkrunner/")
if err != nil {
a.Logger.Debugln(err)
}
}
// MemCheck checks mem percentage
func (a *Agent) MemCheck(data rmm.Check, r *resty.Client) {
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 := r.R().SetBody(payload).Patch("/api/v3/checkrunner/")
if err != nil {
a.Logger.Debugln(err)
}
}
type EventLogCheckResult struct {
ID int `json:"id"`
Log []rmm.EventLogMsg `json:"log"`
}
func (a *Agent) EventLogCheck(data rmm.Check, r *resty.Client) {
log := make([]rmm.EventLogMsg, 0)
evtLog := a.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 := r.R().SetBody(payload).Patch("/api/v3/checkrunner/")
if err != nil {
a.Logger.Debugln(err)
}
}
func (a *Agent) SendPingCheckResult(payload rmm.PingCheckResponse, r *resty.Client) {
_, err := r.R().SetBody(payload).Patch("/api/v3/checkrunner/")
if err != nil {
a.Logger.Debugln(err)
}
}
func (a *Agent) PingCheck(data rmm.Check) (payload rmm.PingCheckResponse) {
payload.ID = data.CheckPK
out, err := DoPing(data.IP)
if err != nil {
a.Logger.Debugln("PingCheck:", err)
payload.Status = "failing"
payload.Output = err.Error()
return
}
payload.Status = out.Status
payload.Output = out.Output
return
}
type WinSvcCheckResult struct {
ID int `json:"id"`
MoreInfo string `json:"more_info"`
Status string `json:"status"`
}
func (a *Agent) SendWinSvcCheckResult(payload WinSvcCheckResult, r *resty.Client) {
_, err := r.R().SetBody(payload).Patch("/api/v3/checkrunner/")
if err != nil {
a.Logger.Debugln(err)
}
}
func (a *Agent) WinSvcCheck(data rmm.Check) (payload WinSvcCheckResult) {
payload.ID = data.CheckPK
status, err := GetServiceStatus(data.ServiceName)
if err != nil {
if data.PassNotExist {
payload.Status = "passing"
} else {
payload.Status = "failing"
}
payload.MoreInfo = err.Error()
a.Logger.Debugln("Service", data.ServiceName, err)
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 := a.ControlService(data.ServiceName, "start")
if ret.Success {
payload.Status = "passing"
payload.MoreInfo = "Status: running"
} else {
payload.Status = "failing"
}
} else {
payload.Status = "failing"
}
}
return
}

71
agent/choco_windows.go Normal file
View file

@ -0,0 +1,71 @@
/*
Copyright 2022 AmidaWare LLC.
Licensed under the Tactical RMM License Version 1.0 (the License).
You may only use the Licensed Software in accordance with the License.
A copy of the License is available at:
https://license.tacticalrmm.com
*/
package agent
import (
"time"
rmm "github.com/amidaware/rmmagent/shared"
"github.com/go-resty/resty/v2"
)
func (a *Agent) InstallChoco() {
var result rmm.ChocoInstalled
result.AgentID = a.AgentID
result.Installed = false
rClient := resty.New()
rClient.SetTimeout(30 * time.Second)
if len(a.Proxy) > 0 {
rClient.SetProxy(a.Proxy)
}
url := "/api/v3/choco/"
r, err := rClient.R().Get("https://chocolatey.org/install.ps1")
if err != nil {
a.Logger.Debugln(err)
a.rClient.R().SetBody(result).Post(url)
return
}
if r.IsError() {
a.rClient.R().SetBody(result).Post(url)
return
}
_, _, exitcode, err := a.RunScript(string(r.Body()), "powershell", []string{}, 900)
if err != nil {
a.Logger.Debugln(err)
a.rClient.R().SetBody(result).Post(url)
return
}
if exitcode != 0 {
a.rClient.R().SetBody(result).Post(url)
return
}
result.Installed = true
a.rClient.R().SetBody(result).Post(url)
}
func (a *Agent) InstallWithChoco(name string) (string, error) {
out, err := CMD("choco.exe", []string{"install", name, "--yes", "--force", "--force-dependencies"}, 1200, false)
if err != nil {
a.Logger.Errorln(err)
return err.Error(), err
}
if out[1] != "" {
return out[1], nil
}
return out[0], nil
}

186
agent/eventlog_windows.go Normal file
View file

@ -0,0 +1,186 @@
/*
Copyright 2022 AmidaWare LLC.
Licensed under the Tactical RMM License Version 1.0 (the License).
You may only use the Licensed Software in accordance with the License.
A copy of the License is available at:
https://license.tacticalrmm.com
*/
package agent
import (
"fmt"
"strings"
"syscall"
"time"
"unicode/utf16"
"unsafe"
rmm "github.com/amidaware/rmmagent/shared"
"github.com/gonutz/w32/v2"
"golang.org/x/sys/windows"
"golang.org/x/sys/windows/registry"
)
func (a *Agent) GetEventLog(logName string, searchLastDays int) []rmm.EventLogMsg {
var (
oldestLog uint32
nextSize uint32
readBytes uint32
)
buf := []byte{0}
size := uint32(1)
ret := make([]rmm.EventLogMsg, 0)
startTime := time.Now().Add(time.Duration(-(time.Duration(searchLastDays)) * (24 * time.Hour)))
h := w32.OpenEventLog("", logName)
defer w32.CloseEventLog(h)
numRecords, _ := w32.GetNumberOfEventLogRecords(h)
GetOldestEventLogRecord(h, &oldestLog)
startNum := numRecords + oldestLog - 1
uid := 0
for i := startNum; i >= oldestLog; i-- {
flags := EVENTLOG_BACKWARDS_READ | EVENTLOG_SEEK_READ
err := ReadEventLog(h, flags, i, &buf[0], size, &readBytes, &nextSize)
if err != nil {
if err != windows.ERROR_INSUFFICIENT_BUFFER {
a.Logger.Debugln(err)
break
}
buf = make([]byte, nextSize)
size = nextSize
err = ReadEventLog(h, flags, i, &buf[0], size, &readBytes, &nextSize)
if err != nil {
a.Logger.Debugln(err)
break
}
}
r := *(*EVENTLOGRECORD)(unsafe.Pointer(&buf[0]))
timeWritten := time.Unix(int64(r.TimeWritten), 0)
if searchLastDays != 0 {
if timeWritten.Before(startTime) {
break
}
}
eventID := r.EventID & 0x0000FFFF
sourceName, _ := bytesToString(buf[unsafe.Sizeof(EVENTLOGRECORD{}):])
eventType := getEventType(r.EventType)
off := uint32(0)
args := make([]*byte, uintptr(r.NumStrings)*unsafe.Sizeof((*uint16)(nil)))
for n := 0; n < int(r.NumStrings); n++ {
args[n] = &buf[r.StringOffset+off]
_, boff := bytesToString(buf[r.StringOffset+off:])
off += boff + 2
}
var argsptr uintptr
if r.NumStrings > 0 {
argsptr = uintptr(unsafe.Pointer(&args[0]))
}
message, _ := getResourceMessage(logName, sourceName, r.EventID, argsptr)
uid++
eventLogMsg := rmm.EventLogMsg{
Source: sourceName,
EventType: eventType,
EventID: eventID,
Message: message,
Time: timeWritten.String(),
UID: uid,
}
ret = append(ret, eventLogMsg)
}
return ret
}
func getEventType(et uint16) string {
switch et {
case windows.EVENTLOG_INFORMATION_TYPE:
return "INFO"
case windows.EVENTLOG_WARNING_TYPE:
return "WARNING"
case windows.EVENTLOG_ERROR_TYPE:
return "ERROR"
case windows.EVENTLOG_SUCCESS:
return "SUCCESS"
case windows.EVENTLOG_AUDIT_SUCCESS:
return "AUDIT_SUCCESS"
case windows.EVENTLOG_AUDIT_FAILURE:
return "AUDIT_FAILURE"
default:
return "Unknown"
}
}
// https://github.com/mackerelio/go-check-plugins/blob/ad7910fdc45ccb892b5e5fda65ba0956c2b2885d/check-windows-eventlog/lib/check-windows-eventlog.go#L219
func bytesToString(b []byte) (string, uint32) {
var i int
s := make([]uint16, len(b)/2)
for i = range s {
s[i] = uint16(b[i*2]) + uint16(b[(i*2)+1])<<8
if s[i] == 0 {
s = s[0:i]
break
}
}
return string(utf16.Decode(s)), uint32(i * 2)
}
// https://github.com/mackerelio/go-check-plugins/blob/ad7910fdc45ccb892b5e5fda65ba0956c2b2885d/check-windows-eventlog/lib/check-windows-eventlog.go#L232
func getResourceMessage(providerName, sourceName string, eventID uint32, argsptr uintptr) (string, error) {
regkey := fmt.Sprintf(
"SYSTEM\\CurrentControlSet\\Services\\EventLog\\%s\\%s",
providerName, sourceName)
key, err := registry.OpenKey(registry.LOCAL_MACHINE, regkey, registry.QUERY_VALUE)
if err != nil {
return "", err
}
defer key.Close()
val, _, err := key.GetStringValue("EventMessageFile")
if err != nil {
return "", err
}
val, err = registry.ExpandString(val)
if err != nil {
return "", err
}
handle, err := LoadLibraryEx(syscall.StringToUTF16Ptr(val), 0,
DONT_RESOLVE_DLL_REFERENCES|LOAD_LIBRARY_AS_DATAFILE)
if err != nil {
return "", err
}
defer syscall.CloseHandle(handle)
msgbuf := make([]byte, 1<<16)
numChars, err := FormatMessage(
syscall.FORMAT_MESSAGE_FROM_SYSTEM|
syscall.FORMAT_MESSAGE_FROM_HMODULE|
syscall.FORMAT_MESSAGE_ARGUMENT_ARRAY,
handle,
eventID,
0,
&msgbuf[0],
uint32(len(msgbuf)),
argsptr)
if err != nil {
return "", err
}
message, _ := bytesToString(msgbuf[:numChars*2])
message = strings.Replace(message, "\r", "", -1)
message = strings.TrimSuffix(message, "\n")
return message, nil
}

298
agent/install.go Normal file
View file

@ -0,0 +1,298 @@
/*
Copyright 2022 AmidaWare LLC.
Licensed under the Tactical RMM License Version 1.0 (the License).
You may only use the Licensed Software in accordance with the License.
A copy of the License is available at:
https://license.tacticalrmm.com
*/
package agent
import (
"fmt"
"io"
"net/url"
"os"
"path/filepath"
"regexp"
"runtime"
"strconv"
"strings"
"time"
"github.com/go-resty/resty/v2"
trmm "github.com/wh1te909/trmm-shared"
)
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
}
func (a *Agent) Install(i *Installer) {
a.checkExistingAndRemove(i.Silent)
i.Headers = map[string]string{
"content-type": "application/json",
"Authorization": fmt.Sprintf("Token %s", i.Token),
}
a.AgentID = GenerateAgentID()
a.Logger.Debugln("Agent ID:", a.AgentID)
u, err := url.Parse(i.RMM)
if err != nil {
a.installerMsg(err.Error(), "error", i.Silent)
}
if u.Scheme != "https" && u.Scheme != "http" {
a.installerMsg("Invalid URL (must contain https or http)", "error", i.Silent)
}
// 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
}
a.Logger.Debugln("API:", i.SaltMaster)
terr := TestTCP(fmt.Sprintf("%s:4222", i.SaltMaster))
if terr != nil {
a.installerMsg(fmt.Sprintf("ERROR: Either port 4222 TCP is not open on your RMM, or nats.service is not running.\n\n%s", terr.Error()), "error", i.Silent)
}
baseURL := u.Scheme + "://" + u.Host
a.Logger.Debugln("Base URL:", baseURL)
iClient := resty.New()
iClient.SetCloseConnection(true)
iClient.SetTimeout(15 * time.Second)
iClient.SetDebug(a.Debug)
iClient.SetHeaders(i.Headers)
// set proxy if applicable
if len(i.Proxy) > 0 {
a.Logger.Infoln("Using proxy:", i.Proxy)
iClient.SetProxy(i.Proxy)
}
creds, cerr := iClient.R().Get(fmt.Sprintf("%s/api/v3/installer/", baseURL))
if cerr != nil {
a.installerMsg(cerr.Error(), "error", i.Silent)
}
if creds.StatusCode() == 401 {
a.installerMsg("Installer token has expired. Please generate a new one.", "error", i.Silent)
}
verPayload := map[string]string{"version": a.Version}
iVersion, ierr := iClient.R().SetBody(verPayload).Post(fmt.Sprintf("%s/api/v3/installer/", baseURL))
if ierr != nil {
a.installerMsg(ierr.Error(), "error", i.Silent)
}
if iVersion.StatusCode() != 200 {
a.installerMsg(DjangoStringResp(iVersion.String()), "error", i.Silent)
}
rClient := resty.New()
rClient.SetCloseConnection(true)
rClient.SetTimeout(i.Timeout * time.Second)
rClient.SetDebug(a.Debug)
// set rest knox headers
rClient.SetHeaders(i.Headers)
// set local cert if applicable
if len(i.Cert) > 0 {
if !trmm.FileExists(i.Cert) {
a.installerMsg(fmt.Sprintf("%s does not exist", i.Cert), "error", i.Silent)
}
rClient.SetRootCertificate(i.Cert)
}
if len(i.Proxy) > 0 {
rClient.SetProxy(i.Proxy)
}
var arch string
switch a.Arch {
case "x86_64":
arch = "64"
case "x86":
arch = "32"
}
var installerMeshSystemEXE string
if len(i.MeshDir) > 0 {
installerMeshSystemEXE = filepath.Join(i.MeshDir, "MeshAgent.exe")
} else {
installerMeshSystemEXE = a.MeshSystemEXE
}
var meshNodeID string
if runtime.GOOS == "windows" && !i.NoMesh {
mesh := filepath.Join(a.ProgramDir, a.MeshInstaller)
if i.LocalMesh == "" {
a.Logger.Infoln("Downloading mesh agent...")
payload := map[string]string{"arch": arch, "plat": a.Platform}
r, err := rClient.R().SetBody(payload).SetOutput(mesh).Post(fmt.Sprintf("%s/api/v3/meshexe/", baseURL))
if err != nil {
a.installerMsg(fmt.Sprintf("Failed to download mesh agent: %s", err.Error()), "error", i.Silent)
}
if r.StatusCode() != 200 {
a.installerMsg(fmt.Sprintf("Unable to download the mesh agent from the RMM. %s", r.String()), "error", i.Silent)
}
} else {
err := copyFile(i.LocalMesh, mesh)
if err != nil {
a.installerMsg(err.Error(), "error", i.Silent)
}
}
a.Logger.Infoln("Installing mesh agent...")
a.Logger.Debugln("Mesh agent:", mesh)
time.Sleep(1 * time.Second)
meshNodeID, err = a.installMesh(mesh, installerMeshSystemEXE, i.Proxy)
if err != nil {
a.installerMsg(fmt.Sprintf("Failed to install mesh agent: %s", err.Error()), "error", i.Silent)
}
}
if len(i.MeshNodeID) > 0 {
meshNodeID = i.MeshNodeID
}
a.Logger.Infoln("Adding agent to dashboard")
// add agent
type NewAgentResp struct {
AgentPK int `json:"pk"`
Token string `json:"token"`
}
agentPayload := map[string]interface{}{
"agent_id": a.AgentID,
"hostname": a.Hostname,
"site": i.SiteID,
"monitoring_type": i.AgentType,
"mesh_node_id": meshNodeID,
"description": i.Description,
"goarch": a.GoArch,
"plat": a.Platform,
}
r, err := rClient.R().SetBody(agentPayload).SetResult(&NewAgentResp{}).Post(fmt.Sprintf("%s/api/v3/newagent/", baseURL))
if err != nil {
a.installerMsg(err.Error(), "error", i.Silent)
}
if r.StatusCode() != 200 {
a.installerMsg(r.String(), "error", i.Silent)
}
agentPK := r.Result().(*NewAgentResp).AgentPK
agentToken := r.Result().(*NewAgentResp).Token
a.Logger.Debugln("Agent token:", agentToken)
a.Logger.Debugln("Agent PK:", agentPK)
createAgentConfig(baseURL, a.AgentID, i.SaltMaster, agentToken, strconv.Itoa(agentPK), i.Cert, i.Proxy, i.MeshDir)
time.Sleep(1 * time.Second)
// refresh our agent with new values
a = New(a.Logger, a.Version)
a.Logger.Debugf("%+v\n", a)
// set new headers, no longer knox auth...use agent auth
rClient.SetHeaders(a.Headers)
time.Sleep(3 * time.Second)
// check in once
a.DoNatsCheckIn()
if runtime.GOOS == "windows" {
// send software api
a.SendSoftware()
a.Logger.Debugln("Creating temp dir")
a.CreateTRMMTempDir()
a.Logger.Debugln("Disabling automatic windows updates")
a.PatchMgmnt(true)
a.Logger.Infoln("Installing service...")
err := a.InstallService()
if err != nil {
a.installerMsg(err.Error(), "error", i.Silent)
}
time.Sleep(1 * time.Second)
a.Logger.Infoln("Starting service...")
out := a.ControlService(winSvcName, "start")
if !out.Success {
a.installerMsg(out.ErrorMsg, "error", i.Silent)
}
a.Logger.Infoln("Adding windows defender exclusions")
a.addDefenderExlusions()
if i.Power {
a.Logger.Infoln("Disabling sleep/hibernate...")
DisableSleepHibernate()
}
if i.Ping {
a.Logger.Infoln("Enabling ping...")
EnablePing()
}
if i.RDP {
a.Logger.Infoln("Enabling RDP...")
EnableRDP()
}
}
a.installerMsg("Installation was successfull!\nAllow a few minutes for the agent to properly display in the RMM", "info", i.Silent)
}
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
}

57
agent/install_linux.go Normal file
View file

@ -0,0 +1,57 @@
/*
Copyright 2022 AmidaWare LLC.
Licensed under the Tactical RMM License Version 1.0 (the License).
You may only use the Licensed Software in accordance with the License.
A copy of the License is available at:
https://license.tacticalrmm.com
*/
package agent
import (
"log"
"github.com/spf13/viper"
)
const (
etcConfig = "/etc/tacticalagent"
)
func (a *Agent) checkExistingAndRemove(silent bool) {}
func (a *Agent) installerMsg(msg, alert string, silent bool) {
if alert == "error" {
a.Logger.Fatalln(msg)
} else {
a.Logger.Info(msg)
}
}
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)
err := viper.SafeWriteConfigAs(etcConfig)
if err != nil {
log.Fatalln("createAgentConfig", err)
}
}
func (a *Agent) addDefenderExlusions() {}
func DisableSleepHibernate() {}
func EnablePing() {}
func EnableRDP() {}

130
agent/install_windows.go Normal file
View file

@ -0,0 +1,130 @@
/*
Copyright 2022 AmidaWare LLC.
Licensed under the Tactical RMM License Version 1.0 (the License).
You may only use the Licensed Software in accordance with the License.
A copy of the License is available at:
https://license.tacticalrmm.com
*/
package agent
import (
"fmt"
"log"
"os"
"path/filepath"
"github.com/gonutz/w32/v2"
"golang.org/x/sys/windows/registry"
)
func createAgentConfig(baseurl, agentid, apiurl, token, agentpk, cert, proxy, meshdir string) {
k, _, err := registry.CreateKey(registry.LOCAL_MACHINE, `SOFTWARE\TacticalRMM`, registry.ALL_ACCESS)
if err != nil {
log.Fatalln("Error creating registry key:", err)
}
defer k.Close()
err = k.SetStringValue("BaseURL", baseurl)
if err != nil {
log.Fatalln("Error creating BaseURL registry key:", err)
}
err = k.SetStringValue("AgentID", agentid)
if err != nil {
log.Fatalln("Error creating AgentID registry key:", err)
}
err = k.SetStringValue("ApiURL", apiurl)
if err != nil {
log.Fatalln("Error creating ApiURL registry key:", err)
}
err = k.SetStringValue("Token", token)
if err != nil {
log.Fatalln("Error creating Token registry key:", err)
}
err = k.SetStringValue("AgentPK", agentpk)
if err != nil {
log.Fatalln("Error creating AgentPK registry key:", err)
}
if len(cert) > 0 {
err = k.SetStringValue("Cert", cert)
if err != nil {
log.Fatalln("Error creating Cert registry key:", err)
}
}
if len(proxy) > 0 {
err = k.SetStringValue("Proxy", proxy)
if err != nil {
log.Fatalln("Error creating Proxy registry key:", err)
}
}
if len(meshdir) > 0 {
err = k.SetStringValue("MeshDir", meshdir)
if err != nil {
log.Fatalln("Error creating MeshDir registry key:", err)
}
}
}
func (a *Agent) 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(a.ProgramDir, a.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 {
a.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)
}
}
func (a *Agent) installerMsg(msg, alert string, silent bool) {
window := w32.GetForegroundWindow()
if !silent && window != 0 {
var (
handle w32.HWND
flags uint
)
switch alert {
case "info":
flags = w32.MB_OK | w32.MB_ICONINFORMATION
case "error":
flags = w32.MB_OK | w32.MB_ICONERROR
default:
flags = w32.MB_OK | w32.MB_ICONINFORMATION
}
w32.MessageBox(handle, msg, "Tactical RMM", flags)
} else {
fmt.Println(msg)
}
if alert == "error" {
a.Logger.Fatalln(msg)
}
}

114
agent/patches_windows.go Normal file
View file

@ -0,0 +1,114 @@
/*
Copyright 2022 AmidaWare LLC.
Licensed under the Tactical RMM License Version 1.0 (the License).
You may only use the Licensed Software in accordance with the License.
A copy of the License is available at:
https://license.tacticalrmm.com
*/
package agent
import (
"fmt"
"time"
rmm "github.com/amidaware/rmmagent/shared"
)
func (a *Agent) GetWinUpdates() {
updates, err := WUAUpdates("IsInstalled=1 or IsInstalled=0 and Type='Software' and IsHidden=0")
if err != nil {
a.Logger.Errorln(err)
return
}
for _, update := range updates {
a.Logger.Debugln("GUID:", update.UpdateID)
a.Logger.Debugln("Downloaded:", update.Downloaded)
a.Logger.Debugln("Installed:", update.Installed)
a.Logger.Debugln("KB:", update.KBArticleIDs)
a.Logger.Debugln("--------------------------------")
}
payload := rmm.WinUpdateResult{AgentID: a.AgentID, Updates: updates}
_, err = a.rClient.R().SetBody(payload).Post("/api/v3/winupdates/")
if err != nil {
a.Logger.Debugln(err)
}
}
func (a *Agent) InstallUpdates(guids []string) {
session, err := NewUpdateSession()
if err != nil {
a.Logger.Errorln(err)
return
}
defer session.Close()
for _, id := range guids {
var result rmm.WinUpdateInstallResult
result.AgentID = a.AgentID
result.UpdateID = id
query := fmt.Sprintf("UpdateID='%s'", id)
a.Logger.Debugln("query:", query)
updts, err := session.GetWUAUpdateCollection(query)
if err != nil {
a.Logger.Errorln(err)
result.Success = false
a.rClient.R().SetBody(result).Patch("/api/v3/winupdates/")
continue
}
defer updts.Release()
updtCnt, err := updts.Count()
if err != nil {
a.Logger.Errorln(err)
result.Success = false
a.rClient.R().SetBody(result).Patch("/api/v3/winupdates/")
continue
}
a.Logger.Debugln("updtCnt:", updtCnt)
if updtCnt == 0 {
superseded := rmm.SupersededUpdate{AgentID: a.AgentID, UpdateID: id}
a.rClient.R().SetBody(superseded).Post("/api/v3/superseded/")
continue
}
for i := 0; i < int(updtCnt); i++ {
u, err := updts.Item(i)
if err != nil {
a.Logger.Errorln(err)
result.Success = false
a.rClient.R().SetBody(result).Patch("/api/v3/winupdates/")
continue
}
a.Logger.Debugln("u:", u)
err = session.InstallWUAUpdate(u)
if err != nil {
a.Logger.Errorln(err)
result.Success = false
a.rClient.R().SetBody(result).Patch("/api/v3/winupdates/")
continue
}
result.Success = true
a.rClient.R().SetBody(result).Patch("/api/v3/winupdates/")
a.Logger.Debugln("Installed windows update with guid", id)
}
}
time.Sleep(5 * time.Second)
needsReboot, err := a.SystemRebootRequired()
if err != nil {
a.Logger.Errorln(err)
}
rebootPayload := rmm.AgentNeedsReboot{AgentID: a.AgentID, NeedsReboot: needsReboot}
_, err = a.rClient.R().SetBody(rebootPayload).Put("/api/v3/winupdates/")
if err != nil {
a.Logger.Debugln("NeedsReboot:", err)
}
}

72
agent/process.go Normal file
View file

@ -0,0 +1,72 @@
/*
Copyright 2022 AmidaWare LLC.
Licensed under the Tactical RMM License Version 1.0 (the License).
You may only use the Licensed Software in accordance with the License.
A copy of the License is available at:
https://license.tacticalrmm.com
*/
package agent
import (
"fmt"
"strings"
rmm "github.com/amidaware/rmmagent/shared"
ps "github.com/elastic/go-sysinfo"
gops "github.com/shirou/gopsutil/v3/process"
)
func (a *Agent) GetProcsRPC() []rmm.ProcessMsg {
ret := make([]rmm.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, rmm.ProcessMsg{
Name: p.Name,
Pid: p.PID,
MemBytes: m.Resident,
Username: user,
UID: i,
CPU: fmt.Sprintf("%.1f", cpu),
})
}
return ret
}
func (a *Agent) KillHungUpdates() {
procs, err := ps.Processes()
if err != nil {
return
}
for _, process := range procs {
p, err := process.Info()
if err != nil {
continue
}
if strings.Contains(p.Exe, "winagent-v") {
a.Logger.Debugln("killing process", p.Exe)
KillProc(int32(p.PID))
}
}
}

507
agent/rpc.go Normal file
View file

@ -0,0 +1,507 @@
/*
Copyright 2022 AmidaWare LLC.
Licensed under the Tactical RMM License Version 1.0 (the License).
You may only use the Licensed Software in accordance with the License.
A copy of the License is available at:
https://license.tacticalrmm.com
*/
package agent
import (
"fmt"
"os"
"runtime"
"strconv"
"sync"
"sync/atomic"
"time"
rmm "github.com/amidaware/rmmagent/shared"
nats "github.com/nats-io/nats.go"
"github.com/ugorji/go/codec"
)
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 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"`
}
var (
agentUpdateLocker uint32
getWinUpdateLocker uint32
installWinUpdateLocker uint32
)
func (a *Agent) RunRPC() {
a.Logger.Infoln("Agent service started")
go a.RunAsService()
var wg sync.WaitGroup
wg.Add(1)
opts := a.setupNatsOptions()
server := fmt.Sprintf("tls://%s:4222", a.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) {
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
}
switch payload.Func {
case "ping":
go func() {
var resp []byte
ret := codec.NewEncoderBytes(&resp, new(codec.MsgpackHandle))
a.Logger.Debugln("pong")
ret.Encode("pong")
msg.Respond(resp)
}()
case "patchmgmt":
go func(p *NatsMsg) {
var resp []byte
ret := codec.NewEncoderBytes(&resp, new(codec.MsgpackHandle))
err := a.PatchMgmnt(p.PatchMgmt)
if err != nil {
a.Logger.Errorln("PatchMgmnt:", err.Error())
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 := a.CreateSchedTask(p.ScheduledTask)
if err != nil {
a.Logger.Errorln(err.Error())
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 := DeleteSchedTask(p.ScheduledTask.Name)
if err != nil {
a.Logger.Errorln(err.Error())
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 := ListSchedTasks()
a.Logger.Debugln(tasks)
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 := a.GetEventLog(p.Data["logname"], days)
a.Logger.Debugln(evtLog)
ret.Encode(evtLog)
msg.Respond(resp)
}(payload)
case "procs":
go func() {
var resp []byte
ret := codec.NewEncoderBytes(&resp, new(codec.MsgpackHandle))
procs := a.GetProcsRPC()
a.Logger.Debugln(procs)
ret.Encode(procs)
msg.Respond(resp)
}()
case "killproc":
go func(p *NatsMsg) {
var resp []byte
ret := codec.NewEncoderBytes(&resp, new(codec.MsgpackHandle))
err := KillProc(p.ProcPID)
if err != nil {
ret.Encode(err.Error())
a.Logger.Debugln(err.Error())
} else {
ret.Encode("ok")
}
msg.Respond(resp)
}(payload)
case "rawcmd":
go func(p *NatsMsg) {
var resp []byte
var resultData rmm.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)
if out[1] != "" {
ret.Encode(out[1])
resultData.Results = out[1]
} else {
ret.Encode(out[0])
resultData.Results = out[0]
}
default:
opts := a.NewCMDOpts()
opts.Shell = p.Data["shell"]
opts.Command = p.Data["command"]
opts.Timeout = time.Duration(p.Timeout)
out := a.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 {
a.rClient.R().SetBody(resultData).Patch(fmt.Sprintf("/api/v3/%d/%s/histresult/", p.ID, a.AgentID))
}
}(payload)
case "winservices":
go func() {
var resp []byte
ret := codec.NewEncoderBytes(&resp, new(codec.MsgpackHandle))
svcs := a.GetServices()
a.Logger.Debugln(svcs)
ret.Encode(svcs)
msg.Respond(resp)
}()
case "winsvcdetail":
go func(p *NatsMsg) {
var resp []byte
ret := codec.NewEncoderBytes(&resp, new(codec.MsgpackHandle))
svc := a.GetServiceDetail(p.Data["name"])
a.Logger.Debugln(svc)
ret.Encode(svc)
msg.Respond(resp)
}(payload)
case "winsvcaction":
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)
ret.Encode(retData)
msg.Respond(resp)
}(payload)
case "editwinsvc":
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)
ret.Encode(retData)
msg.Respond(resp)
}(payload)
case "runscript":
go func(p *NatsMsg) {
var resp []byte
var retData string
var resultData rmm.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)
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()
} 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}
a.rClient.R().SetBody(results).Patch(fmt.Sprintf("/api/v3/%d/%s/histresult/", p.ID, a.AgentID))
}
}(payload)
case "runscriptfull":
go func(p *NatsMsg) {
var resp []byte
var retData rmm.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)
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}
a.rClient.R().SetBody(results).Patch(fmt.Sprintf("/api/v3/%d/%s/histresult/", p.ID, a.AgentID))
}
}(payload)
case "recover":
go func(p *NatsMsg) {
var resp []byte
ret := codec.NewEncoderBytes(&resp, new(codec.MsgpackHandle))
switch p.Data["mode"] {
case "mesh":
a.Logger.Debugln("Recovering mesh")
a.RecoverMesh()
}
ret.Encode("ok")
msg.Respond(resp)
}(payload)
case "softwarelist":
go func() {
var resp []byte
ret := codec.NewEncoderBytes(&resp, new(codec.MsgpackHandle))
sw := a.GetInstalledSoftware()
a.Logger.Debugln(sw)
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" {
CMD("shutdown.exe", []string{"/r", "/t", "5", "/f"}, 15, false)
} else {
opts := a.NewCMDOpts()
opts.Command = "reboot"
a.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()
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)
}()
case "sysinfo":
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)
}
ret.Encode("ok")
msg.Respond(resp)
}()
case "wmi":
go func() {
a.Logger.Debugln("Sending WMI")
a.NatsMessage(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)
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 a.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)
if checkerr != nil {
a.Logger.Errorln("RPC RunChecks", checkerr)
}
}
} else {
ret.Encode("ok")
msg.Respond(resp)
a.Logger.Debugln("Running checks")
a.RunChecks(true)
}
}()
case "runtask":
go func(p *NatsMsg) {
a.Logger.Debugln("Running task")
a.RunTask(p.TaskPK)
}(payload)
case "publicip":
go func() {
var resp []byte
ret := codec.NewEncoderBytes(&resp, new(codec.MsgpackHandle))
ret.Encode(a.PublicIP())
msg.Respond(resp)
}()
case "installpython":
go a.GetPython(true)
case "installchoco":
go a.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)
results := map[string]string{"results": out}
url := fmt.Sprintf("/api/v3/%d/chocoresult/", p.PendingActionPK)
a.rClient.R().SetBody(results).Patch(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)
a.GetWinUpdates()
}
}()
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)
a.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)
a.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)
a.AgentUninstall(p.Code)
nc.Flush()
nc.Close()
os.Exit(0)
}(payload)
}
})
nc.Flush()
if err := nc.LastError(); err != nil {
a.Logger.Errorln(err)
os.Exit(1)
}
wg.Wait()
}

307
agent/services_windows.go Normal file
View file

@ -0,0 +1,307 @@
/*
Copyright 2022 AmidaWare LLC.
Licensed under the Tactical RMM License Version 1.0 (the License).
You may only use the Licensed Software in accordance with the License.
A copy of the License is available at:
https://license.tacticalrmm.com
*/
package agent
import (
"time"
rmm "github.com/amidaware/rmmagent/shared"
trmm "github.com/wh1te909/trmm-shared"
"golang.org/x/sys/windows/svc"
"golang.org/x/sys/windows/svc/mgr"
)
func GetServiceStatus(name string) (string, error) {
conn, err := mgr.Connect()
if err != nil {
return "n/a", err
}
defer conn.Disconnect()
srv, err := conn.OpenService(name)
if err != nil {
return "n/a", err
}
defer srv.Close()
q, err := srv.Query()
if err != nil {
return "n/a", err
}
return serviceStatusText(uint32(q.State)), nil
}
func (a *Agent) ControlService(name, action string) rmm.WinSvcResp {
conn, err := mgr.Connect()
if err != nil {
return rmm.WinSvcResp{Success: false, ErrorMsg: err.Error()}
}
defer conn.Disconnect()
srv, err := conn.OpenService(name)
if err != nil {
return rmm.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 rmm.WinSvcResp{Success: false, ErrorMsg: err.Error()}
}
timeout := time.Now().Add(30 * time.Second)
for status.State != svc.Stopped {
if timeout.Before(time.Now()) {
return rmm.WinSvcResp{Success: false, ErrorMsg: "Timed out waiting for service to stop"}
}
time.Sleep(500 * time.Millisecond)
status, err = srv.Query()
if err != nil {
return rmm.WinSvcResp{Success: false, ErrorMsg: err.Error()}
}
}
return rmm.WinSvcResp{Success: true, ErrorMsg: ""}
case "start":
err := srv.Start()
if err != nil {
return rmm.WinSvcResp{Success: false, ErrorMsg: err.Error()}
}
return rmm.WinSvcResp{Success: true, ErrorMsg: ""}
}
return rmm.WinSvcResp{Success: false, ErrorMsg: "Something went wrong"}
}
func (a *Agent) EditService(name, startupType string) rmm.WinSvcResp {
conn, err := mgr.Connect()
if err != nil {
return rmm.WinSvcResp{Success: false, ErrorMsg: err.Error()}
}
defer conn.Disconnect()
srv, err := conn.OpenService(name)
if err != nil {
return rmm.WinSvcResp{Success: false, ErrorMsg: err.Error()}
}
defer srv.Close()
conf, err := srv.Config()
if err != nil {
return rmm.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 rmm.WinSvcResp{Success: false, ErrorMsg: "Unknown startup type provided"}
}
conf.StartType = startType
if startupType == "autodelay" {
conf.DelayedAutoStart = true
} else if startupType == "auto" {
conf.DelayedAutoStart = false
}
err = srv.UpdateConfig(conf)
if err != nil {
return rmm.WinSvcResp{Success: false, ErrorMsg: err.Error()}
}
return rmm.WinSvcResp{Success: true, ErrorMsg: ""}
}
func (a *Agent) GetServiceDetail(name string) trmm.WindowsService {
ret := trmm.WindowsService{}
conn, err := mgr.Connect()
if err != nil {
a.Logger.Errorln(err)
return ret
}
defer conn.Disconnect()
srv, err := conn.OpenService(name)
if err != nil {
a.Logger.Errorln(err)
return ret
}
defer srv.Close()
q, err := srv.Query()
if err != nil {
a.Logger.Errorln(err)
return ret
}
conf, err := srv.Config()
if err != nil {
a.Logger.Errorln(err)
return ret
}
ret.BinPath = CleanString(conf.BinaryPathName)
ret.Description = CleanString(conf.Description)
ret.DisplayName = CleanString(conf.DisplayName)
ret.Name = name
ret.PID = q.ProcessId
ret.StartType = serviceStartType(uint32(conf.StartType))
ret.Status = serviceStatusText(uint32(q.State))
ret.Username = CleanString(conf.ServiceStartName)
ret.DelayedAutoStart = conf.DelayedAutoStart
return ret
}
// GetServices returns a list of windows services
func (a *Agent) GetServices() []trmm.WindowsService {
ret := make([]trmm.WindowsService, 0)
conn, err := mgr.Connect()
if err != nil {
a.Logger.Debugln(err)
return ret
}
defer conn.Disconnect()
svcs, err := conn.ListServices()
if err != nil {
a.Logger.Debugln(err)
return ret
}
for _, s := range svcs {
srv, err := conn.OpenService(s)
if err != nil {
if err.Error() != "Access is denied." {
a.Logger.Debugln("Open Service", s, err)
}
continue
}
defer srv.Close()
q, err := srv.Query()
if err != nil {
a.Logger.Debugln(err)
continue
}
conf, err := srv.Config()
if err != nil {
a.Logger.Debugln(err)
continue
}
ret = append(ret, trmm.WindowsService{
Name: s,
Status: serviceStatusText(uint32(q.State)),
DisplayName: CleanString(conf.DisplayName),
BinPath: CleanString(conf.BinaryPathName),
Description: CleanString(conf.Description),
Username: CleanString(conf.ServiceStartName),
PID: q.ProcessId,
StartType: serviceStartType(uint32(conf.StartType)),
DelayedAutoStart: conf.DelayedAutoStart,
})
}
return ret
}
// WaitForService will wait for a service to be in X state for X retries
func WaitForService(name string, status string, retries int) {
attempts := 0
for {
stat, err := GetServiceStatus(name)
if err != nil {
attempts++
time.Sleep(5 * time.Second)
} else {
if stat != status {
attempts++
time.Sleep(5 * time.Second)
} else {
attempts = 0
}
}
if attempts == 0 || attempts >= retries {
break
}
}
}
func serviceExists(name string) bool {
conn, err := mgr.Connect()
if err != nil {
return false
}
defer conn.Disconnect()
srv, err := conn.OpenService(name)
if err != nil {
return false
}
defer srv.Close()
return true
}
// https://docs.microsoft.com/en-us/dotnet/api/system.serviceprocess.servicecontrollerstatus?view=dotnet-plat-ext-3.1
func serviceStatusText(num uint32) string {
switch num {
case 1:
return "stopped"
case 2:
return "start_pending"
case 3:
return "stop_pending"
case 4:
return "running"
case 5:
return "continue_pending"
case 6:
return "pause_pending"
case 7:
return "paused"
default:
return "unknown"
}
}
// https://docs.microsoft.com/en-us/dotnet/api/system.serviceprocess.servicestartmode?view=dotnet-plat-ext-3.1
func serviceStartType(num uint32) string {
switch num {
case 0:
return "Boot"
case 1:
return "System"
case 2:
return "Automatic"
case 3:
return "Manual"
case 4:
return "Disabled"
default:
return "Unknown"
}
}

View file

@ -0,0 +1,53 @@
/*
Copyright 2022 AmidaWare LLC.
Licensed under the Tactical RMM License Version 1.0 (the License).
You may only use the Licensed Software in accordance with the License.
A copy of the License is available at:
https://license.tacticalrmm.com
*/
package agent
import (
"fmt"
so "github.com/iamacarpet/go-win64api/shared"
wapf "github.com/wh1te909/go-win64api"
trmm "github.com/wh1te909/trmm-shared"
)
func installedSoftwareList() ([]so.Software, error) {
sw32, err := wapf.GetSoftwareList(`SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall`, "X32")
if err != nil {
return nil, err
}
return sw32, nil
}
func (a *Agent) GetInstalledSoftware() []trmm.WinSoftwareList {
ret := make([]trmm.WinSoftwareList, 0)
sw, err := installedSoftwareList()
if err != nil {
return ret
}
for _, s := range sw {
t := s.InstallDate
ret = append(ret, trmm.WinSoftwareList{
Name: CleanString(s.Name()),
Version: CleanString(s.Version()),
Publisher: CleanString(s.Publisher),
InstallDate: fmt.Sprintf("%02d-%d-%02d", t.Year(), t.Month(), t.Day()),
Size: ByteCountSI(s.EstimatedSize * 1024),
Source: CleanString(s.InstallSource),
Location: CleanString(s.InstallLocation),
Uninstall: CleanString(s.UninstallString),
})
}
return ret
}

View file

@ -0,0 +1,43 @@
/*
Copyright 2022 AmidaWare LLC.
Licensed under the Tactical RMM License Version 1.0 (the License).
You may only use the Licensed Software in accordance with the License.
A copy of the License is available at:
https://license.tacticalrmm.com
*/
package agent
import (
"fmt"
wapi "github.com/iamacarpet/go-win64api"
trmm "github.com/wh1te909/trmm-shared"
)
func (a *Agent) GetInstalledSoftware() []trmm.WinSoftwareList {
ret := make([]trmm.WinSoftwareList, 0)
sw, err := wapi.InstalledSoftwareList()
if err != nil {
return ret
}
for _, s := range sw {
t := s.InstallDate
ret = append(ret, trmm.WinSoftwareList{
Name: CleanString(s.Name()),
Version: CleanString(s.Version()),
Publisher: CleanString(s.Publisher),
InstallDate: fmt.Sprintf("%02d-%d-%02d", t.Year(), t.Month(), t.Day()),
Size: ByteCountSI(s.EstimatedSize * 1024),
Source: CleanString(s.InstallSource),
Location: CleanString(s.InstallLocation),
Uninstall: CleanString(s.UninstallString),
})
}
return ret
}

96
agent/svc.go Normal file
View file

@ -0,0 +1,96 @@
/*
Copyright 2022 AmidaWare LLC.
Licensed under the Tactical RMM License Version 1.0 (the License).
You may only use the Licensed Software in accordance with the License.
A copy of the License is available at:
https://license.tacticalrmm.com
*/
package agent
import (
"fmt"
"sync"
"time"
nats "github.com/nats-io/nats.go"
)
func (a *Agent) RunAsService() {
var wg sync.WaitGroup
wg.Add(1)
go a.AgentSvc()
go a.CheckRunner()
wg.Wait()
}
func (a *Agent) AgentSvc() {
go a.GetPython(false)
a.CreateTRMMTempDir()
a.RunMigrations()
sleepDelay := randRange(14, 22)
a.Logger.Debugf("AgentSvc() sleeping for %v seconds", sleepDelay)
time.Sleep(time.Duration(sleepDelay) * time.Second)
opts := a.setupNatsOptions()
server := fmt.Sprintf("tls://%s:4222", a.ApiURL)
nc, err := nats.Connect(server, opts...)
if err != nil {
a.Logger.Fatalln("AgentSvc() nats.Connect()", err)
}
for _, s := range natsCheckin {
a.NatsMessage(nc, s)
time.Sleep(time.Duration(randRange(100, 400)) * time.Millisecond)
}
a.SyncMeshNodeID()
time.Sleep(time.Duration(randRange(1, 3)) * time.Second)
a.AgentStartup()
a.SendSoftware()
checkInHelloTicker := time.NewTicker(time.Duration(randRange(30, 60)) * time.Second)
checkInAgentInfoTicker := time.NewTicker(time.Duration(randRange(200, 400)) * time.Second)
checkInWinSvcTicker := time.NewTicker(time.Duration(randRange(2400, 3000)) * time.Second)
checkInPubIPTicker := time.NewTicker(time.Duration(randRange(300, 500)) * time.Second)
checkInDisksTicker := time.NewTicker(time.Duration(randRange(1000, 2000)) * time.Second)
checkInSWTicker := time.NewTicker(time.Duration(randRange(2800, 3500)) * time.Second)
checkInWMITicker := time.NewTicker(time.Duration(randRange(3000, 4000)) * time.Second)
syncMeshTicker := time.NewTicker(time.Duration(randRange(800, 1200)) * time.Second)
for {
select {
case <-checkInHelloTicker.C:
a.NatsMessage(nc, "agent-hello")
case <-checkInAgentInfoTicker.C:
a.NatsMessage(nc, "agent-agentinfo")
case <-checkInWinSvcTicker.C:
a.NatsMessage(nc, "agent-winsvc")
case <-checkInPubIPTicker.C:
a.NatsMessage(nc, "agent-publicip")
case <-checkInDisksTicker.C:
a.NatsMessage(nc, "agent-disks")
case <-checkInSWTicker.C:
a.SendSoftware()
case <-checkInWMITicker.C:
a.NatsMessage(nc, "agent-wmi")
case <-syncMeshTicker.C:
a.SyncMeshNodeID()
}
}
}
func (a *Agent) AgentStartup() {
url := "/api/v3/checkin/"
payload := map[string]interface{}{"agent_id": a.AgentID}
_, err := a.rClient.R().SetBody(payload).Post(url)
if err != nil {
a.Logger.Debugln("AgentStartup()", err)
}
}

116
agent/syscall_windows.go Normal file
View file

@ -0,0 +1,116 @@
/*
Copyright 2022 AmidaWare LLC.
Licensed under the Tactical RMM License Version 1.0 (the License).
You may only use the Licensed Software in accordance with the License.
A copy of the License is available at:
https://license.tacticalrmm.com
*/
package agent
import (
"syscall"
"unsafe"
"github.com/gonutz/w32/v2"
"golang.org/x/sys/windows"
)
var _ unsafe.Pointer
var (
modadvapi32 = windows.NewLazySystemDLL("advapi32.dll")
modkernel32 = windows.NewLazySystemDLL("kernel32.dll")
procFormatMessageW = modkernel32.NewProc("FormatMessageW")
procGetOldestEventLogRecord = modadvapi32.NewProc("GetOldestEventLogRecord")
procLoadLibraryExW = modkernel32.NewProc("LoadLibraryExW")
procReadEventLogW = modadvapi32.NewProc("ReadEventLogW")
)
// https://docs.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-eventlogrecord
type EVENTLOGRECORD struct {
Length uint32
Reserved uint32
RecordNumber uint32
TimeGenerated uint32
TimeWritten uint32
EventID uint32
EventType uint16
NumStrings uint16
EventCategory uint16
ReservedFlags uint16
ClosingRecordNumber uint32
StringOffset uint32
UserSidLength uint32
UserSidOffset uint32
DataLength uint32
DataOffset uint32
}
type ReadFlag uint32
const (
EVENTLOG_SEQUENTIAL_READ ReadFlag = 1 << iota
EVENTLOG_SEEK_READ
EVENTLOG_FORWARDS_READ
EVENTLOG_BACKWARDS_READ
)
const (
DONT_RESOLVE_DLL_REFERENCES uint32 = 0x0001
LOAD_LIBRARY_AS_DATAFILE uint32 = 0x0002
)
func FormatMessage(flags uint32, source syscall.Handle, messageID uint32, languageID uint32, buffer *byte, bufferSize uint32, arguments uintptr) (numChars uint32, err error) {
r0, _, e1 := syscall.Syscall9(procFormatMessageW.Addr(), 7, uintptr(flags), uintptr(source), uintptr(messageID), uintptr(languageID), uintptr(unsafe.Pointer(buffer)), uintptr(bufferSize), uintptr(arguments), 0, 0)
numChars = uint32(r0)
if numChars == 0 {
if e1 != 0 {
err = error(e1)
} else {
err = syscall.EINVAL
}
}
return
}
func GetOldestEventLogRecord(eventLog w32.HANDLE, oldestRecord *uint32) (err error) {
r1, _, e1 := syscall.Syscall(procGetOldestEventLogRecord.Addr(), 2, uintptr(eventLog), uintptr(unsafe.Pointer(oldestRecord)), 0)
if r1 == 0 {
if e1 != 0 {
err = error(e1)
} else {
err = syscall.EINVAL
}
}
return
}
func LoadLibraryEx(filename *uint16, file syscall.Handle, flags uint32) (handle syscall.Handle, err error) {
r0, _, e1 := syscall.Syscall(procLoadLibraryExW.Addr(), 3, uintptr(unsafe.Pointer(filename)), uintptr(file), uintptr(flags))
handle = syscall.Handle(r0)
if handle == 0 {
if e1 != 0 {
err = error(e1)
} else {
err = syscall.EINVAL
}
}
return
}
func ReadEventLog(eventLog w32.HANDLE, readFlags ReadFlag, recordOffset uint32, buffer *byte, numberOfBytesToRead uint32, bytesRead *uint32, minNumberOfBytesNeeded *uint32) (err error) {
r1, _, e1 := syscall.Syscall9(procReadEventLogW.Addr(), 7, uintptr(eventLog), uintptr(readFlags), uintptr(recordOffset), uintptr(unsafe.Pointer(buffer)), uintptr(numberOfBytesToRead), uintptr(unsafe.Pointer(bytesRead)), uintptr(unsafe.Pointer(minNumberOfBytesNeeded)), 0, 0)
if r1 == 0 {
if e1 != 0 {
err = error(e1)
} else {
err = syscall.EINVAL
}
}
return
}

354
agent/tasks_windows.go Normal file
View file

@ -0,0 +1,354 @@
/*
Copyright 2022 AmidaWare LLC.
Licensed under the Tactical RMM License Version 1.0 (the License).
You may only use the Licensed Software in accordance with the License.
A copy of the License is available at:
https://license.tacticalrmm.com
*/
package agent
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"
"time"
rmm "github.com/amidaware/rmmagent/shared"
"github.com/amidaware/taskmaster"
"github.com/rickb777/date/period"
)
func (a *Agent) RunTask(id int) error {
data := rmm.AutomatedTask{}
url := fmt.Sprintf("/api/v3/%d/%s/taskrunner/", id, a.AgentID)
r1, gerr := a.rClient.R().Get(url)
if gerr != nil {
a.Logger.Debugln(gerr)
return gerr
}
if r1.IsError() {
a.Logger.Debugln("Run Task:", r1.String())
return nil
}
if err := json.Unmarshal(r1.Body(), &data); err != nil {
a.Logger.Debugln(err)
return err
}
start := time.Now()
type TaskResult struct {
Stdout string `json:"stdout"`
Stderr string `json:"stderr"`
RetCode int `json:"retcode"`
ExecTime float64 `json:"execution_time"`
}
payload := TaskResult{}
// loop through all task actions
for _, action := range data.TaskActions {
action_start := time.Now()
if action.ActionType == "script" {
stdout, stderr, retcode, err := a.RunScript(action.Code, action.Shell, action.Args, action.Timeout)
if err != nil {
a.Logger.Debugln(err)
}
// 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 := CMDShell(action.Shell, []string{}, action.Command, action.Timeout, false)
if err != nil {
a.Logger.Debugln(err)
}
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 {
a.Logger.Debugln("Invalid Action", action)
}
}
payload.ExecTime = time.Since(start).Seconds()
_, perr := a.rClient.R().SetBody(payload).Patch(url)
if perr != nil {
a.Logger.Debugln(perr)
return perr
}
return nil
}
type SchedTask struct {
PK int `json:"pk"`
Type string `json:"type"`
Name string `json:"name"`
Trigger string `json:"trigger"`
Enabled bool `json:"enabled"`
DayInterval taskmaster.DayInterval `json:"day_interval"`
WeekInterval taskmaster.WeekInterval `json:"week_interval"`
DaysOfWeek taskmaster.DayOfWeek `json:"days_of_week"`
DaysOfMonth taskmaster.DayOfMonth `json:"days_of_month"`
RunOnLastDayOfMonth bool `json:"run_on_last_day_of_month"`
MonthsOfYear taskmaster.Month `json:"months_of_year"`
WeeksOfMonth taskmaster.Week `json:"weeks_of_month"`
StartYear int `json:"start_year"`
StartMonth time.Month `json:"start_month"`
StartDay int `json:"start_day"`
StartHour int `json:"start_hour"`
StartMinute int `json:"start_min"`
ExpireYear int `json:"expire_year"`
ExpireMonth time.Month `json:"expire_month"`
ExpireDay int `json:"expire_day"`
ExpireHour int `json:"expire_hour"`
ExpireMinute int `json:"expire_min"`
RandomDelay period.Period `json:"random_delay"`
RepetitionInterval period.Period `json:"repetition_interval"`
RepetitionDuration period.Period `json:"repetition_duration"`
StopAtDurationEnd bool `json:"stop_at_duration_end"`
Path string `json:"path"`
WorkDir string `json:"workdir"`
Args string `json:"args"`
TaskPolicy taskmaster.TaskInstancesPolicy `json:"multiple_instances"`
RunASAPAfterMissed bool `json:"start_when_available"`
DeleteAfter bool `json:"delete_expired_task_after"`
Overwrite bool `json:"overwrite_task"`
}
func (a *Agent) CreateSchedTask(st SchedTask) (bool, error) {
a.Logger.Debugf("%+v\n", st)
conn, err := taskmaster.Connect()
if err != nil {
a.Logger.Errorln(err)
return false, err
}
defer conn.Disconnect()
var trigger taskmaster.Trigger
var action taskmaster.ExecAction
var tasktrigger taskmaster.TaskTrigger
var now = time.Now()
if st.Trigger == "manual" {
tasktrigger = taskmaster.TaskTrigger{
Enabled: st.Enabled,
StartBoundary: now,
}
} else {
tasktrigger = taskmaster.TaskTrigger{
Enabled: st.Enabled,
StartBoundary: time.Date(st.StartYear, st.StartMonth, st.StartDay, st.StartHour, st.StartMinute, 0, 0, now.Location()),
RepetitionPattern: taskmaster.RepetitionPattern{
RepetitionInterval: st.RepetitionInterval,
RepetitionDuration: st.RepetitionDuration,
StopAtDurationEnd: st.StopAtDurationEnd,
},
}
}
if st.ExpireMinute != 0 {
tasktrigger.EndBoundary = time.Date(st.ExpireYear, st.ExpireMonth, st.ExpireDay, st.ExpireHour, st.ExpireMinute, 0, 0, now.Location())
}
var path, workdir, args string
def := conn.NewTaskDefinition()
switch st.Trigger {
case "runonce":
trigger = taskmaster.TimeTrigger{
TaskTrigger: tasktrigger,
RandomDelay: st.RandomDelay,
}
case "daily":
trigger = taskmaster.DailyTrigger{
TaskTrigger: tasktrigger,
DayInterval: st.DayInterval,
RandomDelay: st.RandomDelay,
}
case "weekly":
trigger = taskmaster.WeeklyTrigger{
TaskTrigger: tasktrigger,
DaysOfWeek: st.DaysOfWeek,
WeekInterval: st.WeekInterval,
RandomDelay: st.RandomDelay,
}
case "monthly":
trigger = taskmaster.MonthlyTrigger{
TaskTrigger: tasktrigger,
DaysOfMonth: st.DaysOfMonth,
MonthsOfYear: st.MonthsOfYear,
RandomDelay: st.RandomDelay,
RunOnLastDayOfMonth: st.RunOnLastDayOfMonth,
}
case "monthlydow":
trigger = taskmaster.MonthlyDOWTrigger{
TaskTrigger: tasktrigger,
DaysOfWeek: st.DaysOfWeek,
MonthsOfYear: st.MonthsOfYear,
WeeksOfMonth: st.WeeksOfMonth,
RandomDelay: st.RandomDelay,
}
case "manual":
trigger = taskmaster.TimeTrigger{
TaskTrigger: tasktrigger,
}
}
def.AddTrigger(trigger)
switch st.Type {
case "rmm":
path = winExeName
workdir = a.ProgramDir
args = fmt.Sprintf("-m taskrunner -p %d", st.PK)
case "schedreboot":
path = "shutdown.exe"
workdir = filepath.Join(os.Getenv("SYSTEMROOT"), "System32")
args = "/r /t 5 /f"
case "custom":
path = st.Path
workdir = st.WorkDir
args = st.Args
}
action = taskmaster.ExecAction{
Path: path,
WorkingDir: workdir,
Args: args,
}
def.AddAction(action)
def.Principal.RunLevel = taskmaster.TASK_RUNLEVEL_HIGHEST
def.Principal.LogonType = taskmaster.TASK_LOGON_SERVICE_ACCOUNT
def.Principal.UserID = "SYSTEM"
def.Settings.AllowDemandStart = true
def.Settings.AllowHardTerminate = true
def.Settings.DontStartOnBatteries = false
def.Settings.Enabled = st.Enabled
def.Settings.StopIfGoingOnBatteries = false
def.Settings.WakeToRun = true
def.Settings.MultipleInstances = st.TaskPolicy
if st.DeleteAfter {
def.Settings.DeleteExpiredTaskAfter = "PT15M"
}
if st.RunASAPAfterMissed {
def.Settings.StartWhenAvailable = true
}
_, success, err := conn.CreateTask(fmt.Sprintf("\\%s", st.Name), def, st.Overwrite)
if err != nil {
a.Logger.Errorln(err)
return false, err
}
return success, nil
}
func DeleteSchedTask(name string) error {
conn, err := taskmaster.Connect()
if err != nil {
return err
}
defer conn.Disconnect()
err = conn.DeleteTask(fmt.Sprintf("\\%s", name))
if err != nil {
return err
}
return nil
}
// CleanupSchedTasks removes all tacticalrmm sched tasks during uninstall
func CleanupSchedTasks() {
conn, err := taskmaster.Connect()
if err != nil {
return
}
defer conn.Disconnect()
tasks, err := conn.GetRegisteredTasks()
if err != nil {
return
}
for _, task := range tasks {
if strings.HasPrefix(task.Name, "TacticalRMM_") {
conn.DeleteTask(fmt.Sprintf("\\%s", task.Name))
}
}
tasks.Release()
}
func ListSchedTasks() []string {
ret := make([]string, 0)
conn, err := taskmaster.Connect()
if err != nil {
return ret
}
defer conn.Disconnect()
tasks, err := conn.GetRegisteredTasks()
if err != nil {
return ret
}
for _, task := range tasks {
ret = append(ret, task.Name)
}
tasks.Release()
return ret
}

300
agent/utils.go Normal file
View file

@ -0,0 +1,300 @@
/*
Copyright 2022 AmidaWare LLC.
Licensed under the Tactical RMM License Version 1.0 (the License).
You may only use the Licensed Software in accordance with the License.
A copy of the License is available at:
https://license.tacticalrmm.com
*/
package agent
import (
"archive/zip"
"bytes"
"fmt"
"io"
"math"
"math/rand"
"net"
"os"
"path/filepath"
"runtime"
"strings"
"time"
ps "github.com/elastic/go-sysinfo"
"github.com/go-ping/ping"
"github.com/go-resty/resty/v2"
"github.com/shirou/gopsutil/v3/process"
)
type PingResponse struct {
Status string
Output string
}
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
}
// PublicIP returns the agent's public ip
// Tries 3 times before giving up
func (a *Agent) PublicIP() string {
a.Logger.Debugln("PublicIP start")
client := resty.New()
client.SetTimeout(4 * time.Second)
if len(a.Proxy) > 0 {
client.SetProxy(a.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 {
a.Logger.Debugln("PublicIP err", err)
continue
}
ip = StripAll(r.String())
if !IsValidIP(ip) {
a.Logger.Debugln("PublicIP not valid", 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 := StripAll(r1.String())
if !IsValidIP(ipv4) {
continue
}
a.Logger.Debugln("Forcing ipv4:", ipv4)
return ipv4
}
a.Logger.Debugln("PublicIP return: ", ip)
break
}
return ip
}
// 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)
}
// ShowVersionInfo prints basic debugging info
func ShowVersionInfo(ver string) {
fmt.Println("Tactical RMM Agent:", ver)
fmt.Println("Arch:", runtime.GOARCH)
fmt.Println("Go version:", runtime.Version())
if runtime.GOOS == "windows" {
fmt.Println("Program Directory:", filepath.Join(os.Getenv("ProgramFiles"), progFilesName))
}
}
// TotalRAM returns total RAM in GB
func (a *Agent) TotalRAM() float64 {
host, err := ps.Host()
if err != nil {
return 8.0
}
mem, err := host.Memory()
if err != nil {
return 8.0
}
return math.Ceil(float64(mem.Total) / 1073741824.0)
}
// BootTime returns system boot time as a unix timestamp
func (a *Agent) BootTime() int64 {
host, err := ps.Host()
if err != nil {
return 1000
}
info := host.Info()
return info.BootTime.Unix()
}
// IsValidIP checks for a valid ipv4 or ipv6
func IsValidIP(ip string) bool {
return net.ParseIP(ip) != nil
}
// StripAll strips all whitespace and newline chars
func StripAll(s string) string {
s = strings.TrimSpace(s)
s = strings.Trim(s, "\n")
s = strings.Trim(s, "\r")
return s
}
// KillProc kills a process and its children
func KillProc(pid int32) error {
p, err := process.NewProcess(pid)
if err != nil {
return err
}
children, err := p.Children()
if err == nil {
for _, child := range children {
if err := child.Kill(); err != nil {
continue
}
}
}
if err := p.Kill(); err != nil {
return err
}
return nil
}
// DjangoStringResp removes double quotes from django rest api resp
func DjangoStringResp(resp string) string {
return strings.Trim(resp, `"`)
}
func TestTCP(addr string) error {
conn, err := net.Dial("tcp4", addr)
if err != nil {
return err
}
defer conn.Close()
return nil
}
// CleanString removes invalid utf-8 byte sequences
func CleanString(s string) string {
r := strings.NewReplacer("\x00", "")
s = r.Replace(s)
return strings.ToValidUTF8(s, "")
}
// https://golangcode.com/unzip-files-in-go/
func Unzip(src, dest string) error {
r, err := zip.OpenReader(src)
if err != nil {
return err
}
defer r.Close()
for _, f := range r.File {
// Store filename/path for returning and using later on
fpath := filepath.Join(dest, f.Name)
// Check for ZipSlip. More Info: http://bit.ly/2MsjAWE
if !strings.HasPrefix(fpath, filepath.Clean(dest)+string(os.PathSeparator)) {
return fmt.Errorf("%s: illegal file path", fpath)
}
if f.FileInfo().IsDir() {
// Make Folder
os.MkdirAll(fpath, os.ModePerm)
continue
}
// Make File
if err = os.MkdirAll(filepath.Dir(fpath), os.ModePerm); err != nil {
return err
}
outFile, err := os.OpenFile(fpath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode())
if err != nil {
return err
}
rc, err := f.Open()
if err != nil {
return err
}
_, err = io.Copy(outFile, rc)
// Close the file without defer to close before next iteration of loop
outFile.Close()
rc.Close()
if err != nil {
return err
}
}
return nil
}
// https://yourbasic.org/golang/formatting-byte-size-to-human-readable-format/
func ByteCountSI(b uint64) string {
const unit = 1024
if b < unit {
return fmt.Sprintf("%d B", b)
}
div, exp := int64(unit), 0
for n := b / unit; n >= unit; n /= unit {
div *= unit
exp++
}
return fmt.Sprintf("%.1f %cB",
float64(b)/float64(div), "kMGTPE"[exp])
}
func randRange(min, max int) int {
rand.Seed(time.Now().UnixNano())
return rand.Intn(max-min) + min
}
func randomCheckDelay() {
time.Sleep(time.Duration(randRange(300, 950)) * time.Millisecond)
}

601
agent/wmi_windows.go Normal file
View file

@ -0,0 +1,601 @@
/*
Copyright 2022 AmidaWare LLC.
Licensed under the Tactical RMM License Version 1.0 (the License).
You may only use the Licensed Software in accordance with the License.
A copy of the License is available at:
https://license.tacticalrmm.com
*/
package agent
import (
"encoding/json"
"github.com/StackExchange/wmi"
rmm "github.com/amidaware/rmmagent/shared"
)
func GetWin32_USBController() ([]interface{}, error) {
var dst []rmm.Win32_USBController
ret := make([]interface{}, 0)
q := wmi.CreateQuery(&dst, "")
err := wmi.Query(q, &dst)
if err != nil {
return ret, err
}
for _, val := range dst {
b, err := json.Marshal(val)
if err != nil {
return ret, err
}
// this creates an extra unneeded array but keeping for now
// for backwards compatibility with the python agent
tmp := make([]interface{}, 0)
var un map[string]interface{}
if err := json.Unmarshal(b, &un); err != nil {
return ret, err
}
tmp = append(tmp, un)
ret = append(ret, tmp)
}
return ret, nil
}
func GetWin32_Processor() ([]interface{}, error) {
var (
dstEX []rmm.Win32_ProcessorEX
dst []rmm.Win32_Processor
errEX error
errORIG error
fallback bool = false
)
ret := make([]interface{}, 0)
q := "SELECT * FROM Win32_Processor"
errEX = wmi.Query(q, &dstEX)
if errEX != nil {
errORIG = wmi.Query(q, &dst)
if errORIG != nil {
return ret, errORIG
}
}
if errEX == nil {
for _, val := range dstEX {
b, err := json.Marshal(val)
if err != nil {
fallback = true
break
}
// this creates an extra unneeded array but keeping for now
// for backwards compatibility with the python agent
tmp := make([]interface{}, 0)
var un map[string]interface{}
if err := json.Unmarshal(b, &un); err != nil {
return ret, err
}
tmp = append(tmp, un)
ret = append(ret, tmp)
}
if !fallback {
return ret, nil
}
}
if errORIG == nil {
for _, val := range dst {
b, err := json.Marshal(val)
if err != nil {
return ret, err
}
tmp := make([]interface{}, 0)
var un map[string]interface{}
if err := json.Unmarshal(b, &un); err != nil {
return ret, err
}
tmp = append(tmp, un)
ret = append(ret, tmp)
}
return ret, nil
}
return ret, nil
}
func GetWin32_DesktopMonitor() ([]interface{}, error) {
var dst []rmm.Win32_DesktopMonitor
ret := make([]interface{}, 0)
q := wmi.CreateQuery(&dst, "")
err := wmi.Query(q, &dst)
if err != nil {
return ret, err
}
for _, val := range dst {
b, err := json.Marshal(val)
if err != nil {
return ret, err
}
// this creates an extra unneeded array but keeping for now
// for backwards compatibility with the python agent
tmp := make([]interface{}, 0)
var un map[string]interface{}
if err := json.Unmarshal(b, &un); err != nil {
return ret, err
}
tmp = append(tmp, un)
ret = append(ret, tmp)
}
return ret, nil
}
func GetWin32_NetworkAdapter() ([]interface{}, error) {
var dst []rmm.Win32_NetworkAdapter
ret := make([]interface{}, 0)
q := wmi.CreateQuery(&dst, "")
err := wmi.Query(q, &dst)
if err != nil {
return ret, err
}
for _, val := range dst {
b, err := json.Marshal(val)
if err != nil {
return ret, err
}
// this creates an extra unneeded array but keeping for now
// for backwards compatibility with the python agent
tmp := make([]interface{}, 0)
var un map[string]interface{}
if err := json.Unmarshal(b, &un); err != nil {
return ret, err
}
tmp = append(tmp, un)
ret = append(ret, tmp)
}
return ret, nil
}
func GetWin32_DiskDrive() ([]interface{}, error) {
var dst []rmm.Win32_DiskDrive
ret := make([]interface{}, 0)
q := wmi.CreateQuery(&dst, "")
err := wmi.Query(q, &dst)
if err != nil {
return ret, err
}
for _, val := range dst {
b, err := json.Marshal(val)
if err != nil {
return ret, err
}
// this creates an extra unneeded array but keeping for now
// for backwards compatibility with the python agent
tmp := make([]interface{}, 0)
var un map[string]interface{}
if err := json.Unmarshal(b, &un); err != nil {
return ret, err
}
tmp = append(tmp, un)
ret = append(ret, tmp)
}
return ret, nil
}
func GetWin32_ComputerSystemProduct() ([]interface{}, error) {
var dst []rmm.Win32_ComputerSystemProduct
ret := make([]interface{}, 0)
q := wmi.CreateQuery(&dst, "")
err := wmi.Query(q, &dst)
if err != nil {
return ret, err
}
for _, val := range dst {
b, err := json.Marshal(val)
if err != nil {
return ret, err
}
// this creates an extra unneeded array but keeping for now
// for backwards compatibility with the python agent
tmp := make([]interface{}, 0)
var un map[string]interface{}
if err := json.Unmarshal(b, &un); err != nil {
return ret, err
}
tmp = append(tmp, un)
ret = append(ret, tmp)
}
return ret, nil
}
func GetWin32_BIOS() ([]interface{}, error) {
var (
dstEX []rmm.Win32_BIOSEX
dst []rmm.Win32_BIOS
errEX error
errORIG error
fallback bool = false
)
ret := make([]interface{}, 0)
q := "SELECT * FROM Win32_BIOS"
errEX = wmi.Query(q, &dstEX)
if errEX != nil {
errORIG = wmi.Query(q, &dst)
if errORIG != nil {
return ret, errORIG
}
}
if errEX == nil {
for _, val := range dstEX {
b, err := json.Marshal(val)
if err != nil {
fallback = true
break
}
// this creates an extra unneeded array but keeping for now
// for backwards compatibility with the python agent
tmp := make([]interface{}, 0)
var un map[string]interface{}
if err := json.Unmarshal(b, &un); err != nil {
return ret, err
}
tmp = append(tmp, un)
ret = append(ret, tmp)
}
if !fallback {
return ret, nil
}
}
if errORIG == nil {
for _, val := range dst {
b, err := json.Marshal(val)
if err != nil {
return ret, err
}
tmp := make([]interface{}, 0)
var un map[string]interface{}
if err := json.Unmarshal(b, &un); err != nil {
return ret, err
}
tmp = append(tmp, un)
ret = append(ret, tmp)
}
return ret, nil
}
return ret, nil
}
func GetWin32_ComputerSystem() ([]interface{}, error) {
var (
dstEX []rmm.Win32_ComputerSystemEX
dst []rmm.Win32_ComputerSystem
errEX error
errORIG error
fallback bool = false
)
ret := make([]interface{}, 0)
q := "SELECT * FROM Win32_ComputerSystem"
errEX = wmi.Query(q, &dstEX)
if errEX != nil {
errORIG = wmi.Query(q, &dst)
if errORIG != nil {
return ret, errORIG
}
}
if errEX == nil {
for _, val := range dstEX {
b, err := json.Marshal(val)
if err != nil {
fallback = true
break
}
// this creates an extra unneeded array but keeping for now
// for backwards compatibility with the python agent
tmp := make([]interface{}, 0)
var un map[string]interface{}
if err := json.Unmarshal(b, &un); err != nil {
return ret, err
}
tmp = append(tmp, un)
ret = append(ret, tmp)
}
if !fallback {
return ret, nil
}
}
if errORIG == nil {
for _, val := range dst {
b, err := json.Marshal(val)
if err != nil {
return ret, err
}
tmp := make([]interface{}, 0)
var un map[string]interface{}
if err := json.Unmarshal(b, &un); err != nil {
return ret, err
}
tmp = append(tmp, un)
ret = append(ret, tmp)
}
return ret, nil
}
return ret, nil
}
func GetWin32_NetworkAdapterConfiguration() ([]interface{}, error) {
var dst []rmm.Win32_NetworkAdapterConfiguration
ret := make([]interface{}, 0)
q := wmi.CreateQuery(&dst, "")
err := wmi.Query(q, &dst)
if err != nil {
return ret, err
}
for _, val := range dst {
b, err := json.Marshal(val)
if err != nil {
return ret, err
}
// this creates an extra unneeded array but keeping for now
// for backwards compatibility with the python agent
tmp := make([]interface{}, 0)
var un map[string]interface{}
if err := json.Unmarshal(b, &un); err != nil {
return ret, err
}
tmp = append(tmp, un)
ret = append(ret, tmp)
}
return ret, nil
}
func GetWin32_PhysicalMemory() ([]interface{}, error) {
var (
dstEX []rmm.Win32_PhysicalMemoryEX
dst []rmm.Win32_PhysicalMemory
errEX error
errORIG error
fallback bool = false
)
ret := make([]interface{}, 0)
q := "SELECT * FROM Win32_PhysicalMemory"
errEX = wmi.Query(q, &dstEX)
if errEX != nil {
errORIG = wmi.Query(q, &dst)
if errORIG != nil {
return ret, errORIG
}
}
if errEX == nil {
for _, val := range dstEX {
b, err := json.Marshal(val)
if err != nil {
fallback = true
break
}
// this creates an extra unneeded array but keeping for now
// for backwards compatibility with the python agent
tmp := make([]interface{}, 0)
var un map[string]interface{}
if err := json.Unmarshal(b, &un); err != nil {
return ret, err
}
tmp = append(tmp, un)
ret = append(ret, tmp)
}
if !fallback {
return ret, nil
}
}
if errORIG == nil {
for _, val := range dst {
b, err := json.Marshal(val)
if err != nil {
return ret, err
}
tmp := make([]interface{}, 0)
var un map[string]interface{}
if err := json.Unmarshal(b, &un); err != nil {
return ret, err
}
tmp = append(tmp, un)
ret = append(ret, tmp)
}
return ret, nil
}
return ret, nil
}
func GetWin32_OperatingSystem() ([]interface{}, error) {
var dst []rmm.Win32_OperatingSystem
ret := make([]interface{}, 0)
q := wmi.CreateQuery(&dst, "")
err := wmi.Query(q, &dst)
if err != nil {
return ret, err
}
for _, val := range dst {
b, err := json.Marshal(val)
if err != nil {
return ret, err
}
// this creates an extra unneeded array but keeping for now
// for backwards compatibility with the python agent
tmp := make([]interface{}, 0)
var un map[string]interface{}
if err := json.Unmarshal(b, &un); err != nil {
return ret, err
}
tmp = append(tmp, un)
ret = append(ret, tmp)
}
return ret, nil
}
func GetWin32_BaseBoard() ([]interface{}, error) {
var dst []rmm.Win32_BaseBoard
ret := make([]interface{}, 0)
q := wmi.CreateQuery(&dst, "")
err := wmi.Query(q, &dst)
if err != nil {
return ret, err
}
for _, val := range dst {
b, err := json.Marshal(val)
if err != nil {
return ret, err
}
// this creates an extra unneeded array but keeping for now
// for backwards compatibility with the python agent
tmp := make([]interface{}, 0)
var un map[string]interface{}
if err := json.Unmarshal(b, &un); err != nil {
return ret, err
}
tmp = append(tmp, un)
ret = append(ret, tmp)
}
return ret, nil
}
func GetWin32_VideoController() ([]interface{}, error) {
var dst []rmm.Win32_VideoController
ret := make([]interface{}, 0)
q := wmi.CreateQuery(&dst, "")
err := wmi.Query(q, &dst)
if err != nil {
return ret, err
}
for _, val := range dst {
b, err := json.Marshal(val)
if err != nil {
return ret, err
}
// this creates an extra unneeded array but keeping for now
// for backwards compatibility with the python agent
tmp := make([]interface{}, 0)
var un map[string]interface{}
if err := json.Unmarshal(b, &un); err != nil {
return ret, err
}
tmp = append(tmp, un)
ret = append(ret, tmp)
}
return ret, nil
}
func (a *Agent) GetWMIInfo() map[string]interface{} {
wmiInfo := make(map[string]interface{})
compSysProd, err := GetWin32_ComputerSystemProduct()
if err != nil {
a.Logger.Debugln(err)
}
compSys, err := GetWin32_ComputerSystem()
if err != nil {
a.Logger.Debugln(err)
}
netAdaptConfig, err := GetWin32_NetworkAdapterConfiguration()
if err != nil {
a.Logger.Debugln(err)
}
physMem, err := GetWin32_PhysicalMemory()
if err != nil {
a.Logger.Debugln(err)
}
winOS, err := GetWin32_OperatingSystem()
if err != nil {
a.Logger.Debugln(err)
}
baseBoard, err := GetWin32_BaseBoard()
if err != nil {
a.Logger.Debugln(err)
}
bios, err := GetWin32_BIOS()
if err != nil {
a.Logger.Debugln(err)
}
disk, err := GetWin32_DiskDrive()
if err != nil {
a.Logger.Debugln(err)
}
netAdapt, err := GetWin32_NetworkAdapter()
if err != nil {
a.Logger.Debugln(err)
}
desktopMon, err := GetWin32_DesktopMonitor()
if err != nil {
a.Logger.Debugln(err)
}
cpu, err := GetWin32_Processor()
if err != nil {
a.Logger.Debugln(err)
}
usb, err := GetWin32_USBController()
if err != nil {
a.Logger.Debugln(err)
}
graphics, err := GetWin32_VideoController()
if err != nil {
a.Logger.Debugln(err)
}
wmiInfo["comp_sys_prod"] = compSysProd
wmiInfo["comp_sys"] = compSys
wmiInfo["network_config"] = netAdaptConfig
wmiInfo["mem"] = physMem
wmiInfo["os"] = winOS
wmiInfo["base_board"] = baseBoard
wmiInfo["bios"] = bios
wmiInfo["disk"] = disk
wmiInfo["network_adapter"] = netAdapt
wmiInfo["desktop_monitor"] = desktopMon
wmiInfo["cpu"] = cpu
wmiInfo["usb"] = usb
wmiInfo["graphics"] = graphics
return wmiInfo
}

478
agent/wua_windows.go Normal file
View file

@ -0,0 +1,478 @@
/*
Copyright 2022 AmidaWare LLC.
Licensed under the Tactical RMM License Version 1.0 (the License).
You may only use the Licensed Software in accordance with the License.
A copy of the License is available at:
https://license.tacticalrmm.com
*/
// Copyright 2018 Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// code taken from https://github.com/GoogleCloudPlatform/osconfig/tree/master/ospatch
// and modified by https://github.com/wh1te909
package agent
import (
"fmt"
"sync"
rmm "github.com/amidaware/rmmagent/shared"
ole "github.com/go-ole/go-ole"
"github.com/go-ole/go-ole/oleutil"
)
const (
S_OK = 0
S_FALSE = 1
)
var wuaSession sync.Mutex
// IUpdateSession is a an IUpdateSession.
type IUpdateSession struct {
*ole.IDispatch
}
func (s *IUpdateSession) Close() {
if s.IDispatch != nil {
s.IDispatch.Release()
}
ole.CoUninitialize()
wuaSession.Unlock()
}
func NewUpdateSession() (*IUpdateSession, error) {
wuaSession.Lock()
if err := ole.CoInitializeEx(0, ole.COINIT_MULTITHREADED); err != nil {
e, ok := err.(*ole.OleError)
// S_OK and S_FALSE are both are Success codes.
// https://docs.microsoft.com/en-us/windows/win32/learnwin32/error-handling-in-com
if !ok || (e.Code() != S_OK && e.Code() != S_FALSE) {
wuaSession.Unlock()
return nil, fmt.Errorf(`ole.CoInitializeEx(0, ole.COINIT_MULTITHREADED): %v`, err)
}
}
s := &IUpdateSession{}
unknown, err := oleutil.CreateObject("Microsoft.Update.Session")
if err != nil {
s.Close()
return nil, fmt.Errorf(`oleutil.CreateObject("Microsoft.Update.Session"): %v`, err)
}
disp, err := unknown.QueryInterface(ole.IID_IDispatch)
if err != nil {
unknown.Release()
s.Close()
return nil, fmt.Errorf(`error creating Dispatch object from Microsoft.Update.Session connection: %v`, err)
}
s.IDispatch = disp
return s, nil
}
// InstallWUAUpdate install a WIndows update.
func (s *IUpdateSession) InstallWUAUpdate(updt *IUpdate) error {
_, err := updt.GetProperty("Title")
if err != nil {
return fmt.Errorf(`updt.GetProperty("Title"): %v`, err)
}
updts, err := NewUpdateCollection()
if err != nil {
return err
}
defer updts.Release()
eula, err := updt.GetProperty("EulaAccepted")
if err != nil {
return fmt.Errorf(`updt.GetProperty("EulaAccepted"): %v`, err)
}
// https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-oaut/7b39eb24-9d39-498a-bcd8-75c38e5823d0
if eula.Val == 0 {
if _, err := updt.CallMethod("AcceptEula"); err != nil {
return fmt.Errorf(`updt.CallMethod("AcceptEula"): %v`, err)
}
}
if err := updts.Add(updt); err != nil {
return err
}
if err := s.DownloadWUAUpdateCollection(updts); err != nil {
return fmt.Errorf("DownloadWUAUpdateCollection error: %v", err)
}
if err := s.InstallWUAUpdateCollection(updts); err != nil {
return fmt.Errorf("InstallWUAUpdateCollection error: %v", err)
}
return nil
}
func NewUpdateCollection() (*IUpdateCollection, error) {
updateCollObj, err := oleutil.CreateObject("Microsoft.Update.UpdateColl")
if err != nil {
return nil, fmt.Errorf(`oleutil.CreateObject("Microsoft.Update.UpdateColl"): %v`, err)
}
defer updateCollObj.Release()
updateColl, err := updateCollObj.IDispatch(ole.IID_IDispatch)
if err != nil {
return nil, err
}
return &IUpdateCollection{IDispatch: updateColl}, nil
}
type IUpdateCollection struct {
*ole.IDispatch
}
type IUpdate struct {
*ole.IDispatch
}
func (c *IUpdateCollection) Add(updt *IUpdate) error {
if _, err := c.CallMethod("Add", updt.IDispatch); err != nil {
return fmt.Errorf(`IUpdateCollection.CallMethod("Add", updt): %v`, err)
}
return nil
}
func (c *IUpdateCollection) RemoveAt(i int) error {
if _, err := c.CallMethod("RemoveAt", i); err != nil {
return fmt.Errorf(`IUpdateCollection.CallMethod("RemoveAt", %d): %v`, i, err)
}
return nil
}
func (c *IUpdateCollection) Count() (int32, error) {
return GetCount(c.IDispatch)
}
func (c *IUpdateCollection) Item(i int) (*IUpdate, error) {
updtRaw, err := c.GetProperty("Item", i)
if err != nil {
return nil, fmt.Errorf(`IUpdateCollection.GetProperty("Item", %d): %v`, i, err)
}
return &IUpdate{IDispatch: updtRaw.ToIDispatch()}, nil
}
// GetCount returns the Count property.
func GetCount(dis *ole.IDispatch) (int32, error) {
countRaw, err := dis.GetProperty("Count")
if err != nil {
return 0, fmt.Errorf(`IDispatch.GetProperty("Count"): %v`, err)
}
count, _ := countRaw.Value().(int32)
return count, nil
}
func (u *IUpdate) kbaIDs() ([]string, error) {
kbArticleIDsRaw, err := u.GetProperty("KBArticleIDs")
if err != nil {
return nil, fmt.Errorf(`IUpdate.GetProperty("KBArticleIDs"): %v`, err)
}
kbArticleIDs := kbArticleIDsRaw.ToIDispatch()
defer kbArticleIDs.Release()
count, err := GetCount(kbArticleIDs)
if err != nil {
return nil, err
}
if count == 0 {
return nil, nil
}
var ss []string
for i := 0; i < int(count); i++ {
item, err := kbArticleIDs.GetProperty("Item", i)
if err != nil {
return nil, fmt.Errorf(`kbArticleIDs.GetProperty("Item", %d): %v`, i, err)
}
ss = append(ss, item.ToString())
}
return ss, nil
}
func (u *IUpdate) categories() ([]string, []string, error) {
catRaw, err := u.GetProperty("Categories")
if err != nil {
return nil, nil, fmt.Errorf(`IUpdate.GetProperty("Categories"): %v`, err)
}
cat := catRaw.ToIDispatch()
defer cat.Release()
count, err := GetCount(cat)
if err != nil {
return nil, nil, err
}
if count == 0 {
return nil, nil, nil
}
var cns, cids []string
for i := 0; i < int(count); i++ {
itemRaw, err := cat.GetProperty("Item", i)
if err != nil {
return nil, nil, fmt.Errorf(`cat.GetProperty("Item", %d): %v`, i, err)
}
item := itemRaw.ToIDispatch()
defer item.Release()
name, err := item.GetProperty("Name")
if err != nil {
return nil, nil, fmt.Errorf(`item.GetProperty("Name"): %v`, err)
}
categoryID, err := item.GetProperty("CategoryID")
if err != nil {
return nil, nil, fmt.Errorf(`item.GetProperty("CategoryID"): %v`, err)
}
cns = append(cns, name.ToString())
cids = append(cids, categoryID.ToString())
}
return cns, cids, nil
}
func (u *IUpdate) moreInfoURLs() ([]string, error) {
moreInfoURLsRaw, err := u.GetProperty("MoreInfoURLs")
if err != nil {
return nil, fmt.Errorf(`IUpdate.GetProperty("MoreInfoURLs"): %v`, err)
}
moreInfoURLs := moreInfoURLsRaw.ToIDispatch()
defer moreInfoURLs.Release()
count, err := GetCount(moreInfoURLs)
if err != nil {
return nil, err
}
if count == 0 {
return nil, nil
}
var ss []string
for i := 0; i < int(count); i++ {
item, err := moreInfoURLs.GetProperty("Item", i)
if err != nil {
return nil, fmt.Errorf(`moreInfoURLs.GetProperty("Item", %d): %v`, i, err)
}
ss = append(ss, item.ToString())
}
return ss, nil
}
func (c *IUpdateCollection) extractPkg(item int) (*rmm.WUAPackage, error) {
updt, err := c.Item(item)
if err != nil {
return nil, err
}
defer updt.Release()
title, err := updt.GetProperty("Title")
if err != nil {
return nil, fmt.Errorf(`updt.GetProperty("Title"): %v`, err)
}
description, err := updt.GetProperty("Description")
if err != nil {
return nil, fmt.Errorf(`updt.GetProperty("Description"): %v`, err)
}
kbArticleIDs, err := updt.kbaIDs()
if err != nil {
return nil, err
}
categories, categoryIDs, err := updt.categories()
if err != nil {
return nil, err
}
moreInfoURLs, err := updt.moreInfoURLs()
if err != nil {
return nil, err
}
supportURL, err := updt.GetProperty("SupportURL")
if err != nil {
return nil, fmt.Errorf(`updt.GetProperty("SupportURL"): %v`, err)
}
identityRaw, err := updt.GetProperty("Identity")
if err != nil {
return nil, fmt.Errorf(`updt.GetProperty("Identity"): %v`, err)
}
identity := identityRaw.ToIDispatch()
defer identity.Release()
revisionNumber, err := identity.GetProperty("RevisionNumber")
if err != nil {
return nil, fmt.Errorf(`identity.GetProperty("RevisionNumber"): %v`, err)
}
updateID, err := identity.GetProperty("UpdateID")
if err != nil {
return nil, fmt.Errorf(`identity.GetProperty("UpdateID"): %v`, err)
}
severity, err := updt.GetProperty("MsrcSeverity")
if err != nil {
return nil, fmt.Errorf(`updt.GetProperty("MsrcSeverity"): %v`, err)
}
isInstalled, err := updt.GetProperty("IsInstalled")
if err != nil {
return nil, fmt.Errorf(`updt.GetProperty("IsInstalled"): %v`, err)
}
isDownloaded, err := updt.GetProperty("IsDownloaded")
if err != nil {
return nil, fmt.Errorf(`updt.GetProperty("IsDownloaded"): %v`, err)
}
return &rmm.WUAPackage{
Title: title.ToString(),
Description: description.ToString(),
SupportURL: supportURL.ToString(),
KBArticleIDs: kbArticleIDs,
UpdateID: updateID.ToString(),
Categories: categories,
CategoryIDs: categoryIDs,
MoreInfoURLs: moreInfoURLs,
Severity: severity.ToString(),
RevisionNumber: int32(revisionNumber.Val),
Downloaded: isDownloaded.Value().(bool),
Installed: isInstalled.Value().(bool),
}, nil
}
// WUAUpdates queries the Windows Update Agent API searcher with the provided query.
func WUAUpdates(query string) ([]rmm.WUAPackage, error) {
session, err := NewUpdateSession()
if err != nil {
return nil, fmt.Errorf("error creating NewUpdateSession: %v", err)
}
defer session.Close()
updts, err := session.GetWUAUpdateCollection(query)
if err != nil {
return nil, fmt.Errorf("error calling GetWUAUpdateCollection with query %q: %v", query, err)
}
defer updts.Release()
updtCnt, err := updts.Count()
if err != nil {
return nil, err
}
if updtCnt == 0 {
return nil, nil
}
var packages []rmm.WUAPackage
for i := 0; i < int(updtCnt); i++ {
pkg, err := updts.extractPkg(i)
if err != nil {
return nil, err
}
packages = append(packages, *pkg)
}
return packages, nil
}
// DownloadWUAUpdateCollection downloads all updates in a IUpdateCollection
func (s *IUpdateSession) DownloadWUAUpdateCollection(updates *IUpdateCollection) error {
// returns IUpdateDownloader
// https://docs.microsoft.com/en-us/windows/desktop/api/wuapi/nn-wuapi-iupdatedownloader
downloaderRaw, err := s.CallMethod("CreateUpdateDownloader")
if err != nil {
return fmt.Errorf("error calling method CreateUpdateDownloader on IUpdateSession: %v", err)
}
downloader := downloaderRaw.ToIDispatch()
defer downloader.Release()
if _, err := downloader.PutProperty("Updates", updates.IDispatch); err != nil {
return fmt.Errorf("error calling PutProperty Updates on IUpdateDownloader: %v", err)
}
if _, err := downloader.CallMethod("Download"); err != nil {
return fmt.Errorf("error calling method Download on IUpdateDownloader: %v", err)
}
return nil
}
// InstallWUAUpdateCollection installs all updates in a IUpdateCollection
func (s *IUpdateSession) InstallWUAUpdateCollection(updates *IUpdateCollection) error {
// returns IUpdateInstallersession *ole.IDispatch,
// https://docs.microsoft.com/en-us/windows/desktop/api/wuapi/nf-wuapi-iupdatesession-createupdateinstaller
installerRaw, err := s.CallMethod("CreateUpdateInstaller")
if err != nil {
return fmt.Errorf("error calling method CreateUpdateInstaller on IUpdateSession: %v", err)
}
installer := installerRaw.ToIDispatch()
defer installer.Release()
if _, err := installer.PutProperty("Updates", updates.IDispatch); err != nil {
return fmt.Errorf("error calling PutProperty Updates on IUpdateInstaller: %v", err)
}
// TODO: Look into using the async methods and attempt to track/log progress.
if _, err := installer.CallMethod("Install"); err != nil {
return fmt.Errorf("error calling method Install on IUpdateInstaller: %v", err)
}
return nil
}
// GetWUAUpdateCollection queries the Windows Update Agent API searcher with the provided query
// and returns a IUpdateCollection.
func (s *IUpdateSession) GetWUAUpdateCollection(query string) (*IUpdateCollection, error) {
// returns IUpdateSearcher
// https://msdn.microsoft.com/en-us/library/windows/desktop/aa386515(v=vs.85).aspx
searcherRaw, err := s.CallMethod("CreateUpdateSearcher")
if err != nil {
return nil, fmt.Errorf("error calling CreateUpdateSearcher: %v", err)
}
searcher := searcherRaw.ToIDispatch()
defer searcher.Release()
// returns ISearchResult
// https://msdn.microsoft.com/en-us/library/windows/desktop/aa386077(v=vs.85).aspx
resultRaw, err := searcher.CallMethod("Search", query)
if err != nil {
return nil, fmt.Errorf("error calling method Search on IUpdateSearcher: %v", err)
}
result := resultRaw.ToIDispatch()
defer result.Release()
// returns IUpdateCollection
// https://msdn.microsoft.com/en-us/library/windows/desktop/aa386107(v=vs.85).aspx
updtsRaw, err := result.GetProperty("Updates")
if err != nil {
return nil, fmt.Errorf("error calling GetProperty Updates on ISearchResult: %v", err)
}
return &IUpdateCollection{IDispatch: updtsRaw.ToIDispatch()}, nil
}