v2.0.0
This commit is contained in:
commit
e455af7f1f
33 changed files with 8471 additions and 0 deletions
470
agent/agent.go
Normal file
470
agent/agent.go
Normal 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
462
agent/agent_linux.go
Normal 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
853
agent/agent_windows.go
Normal 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
93
agent/checkin.go
Normal 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
379
agent/checks.go
Normal 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
71
agent/choco_windows.go
Normal 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
186
agent/eventlog_windows.go
Normal 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
298
agent/install.go
Normal 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
57
agent/install_linux.go
Normal 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
130
agent/install_windows.go
Normal 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
114
agent/patches_windows.go
Normal 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
72
agent/process.go
Normal 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
507
agent/rpc.go
Normal 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
307
agent/services_windows.go
Normal 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"
|
||||
}
|
||||
}
|
||||
53
agent/software_windows_386.go
Normal file
53
agent/software_windows_386.go
Normal 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
|
||||
}
|
||||
43
agent/software_windows_amd64.go
Normal file
43
agent/software_windows_amd64.go
Normal 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
96
agent/svc.go
Normal 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
116
agent/syscall_windows.go
Normal 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
354
agent/tasks_windows.go
Normal 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
300
agent/utils.go
Normal 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
601
agent/wmi_windows.go
Normal 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
478
agent/wua_windows.go
Normal 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
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue