mirror of
https://github.com/SoarinFerret/trmm-lam.git
synced 2025-11-29 00:23:37 +00:00
inital
This commit is contained in:
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
.devbox/
|
||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2025 Cody Ernesti
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
127
cmd/install.go
Normal file
127
cmd/install.go
Normal file
@@ -0,0 +1,127 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/user"
|
||||
"os/exec"
|
||||
|
||||
"github.com/soarinferret/trmm-lam/internal/tacticalrmm"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/pterm/pterm"
|
||||
|
||||
)
|
||||
|
||||
var installCmd = &cobra.Command{
|
||||
Use: "install",
|
||||
Aliases: []string{"i"},
|
||||
Short: "Install TRMM Agent on Linux",
|
||||
Long: ``,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
|
||||
url, _ := cmd.Flags().GetString("url")
|
||||
apikey, _ := cmd.Flags().GetString("api-key")
|
||||
client, _ := cmd.Flags().GetInt("client")
|
||||
site, _ := cmd.Flags().GetInt("site")
|
||||
agentDlUrl, _ := cmd.Flags().GetString("agent-download-url")
|
||||
agentType, _ := cmd.Flags().GetString("type")
|
||||
force, _ := cmd.Flags().GetBool("force")
|
||||
|
||||
if agentType != "server" && agentType != "workstation" {
|
||||
pterm.Error.Println("Invalid agent type. Must be server or workstation")
|
||||
return
|
||||
}
|
||||
|
||||
rmm := tacticalrmm.New(url, apikey, agentDlUrl)
|
||||
|
||||
if client == -1 || site == -1 {
|
||||
// interactively select client and site
|
||||
|
||||
clients, err := rmm.GetClients()
|
||||
if err != nil {
|
||||
pterm.Error.Println("Failed to retrieve clients:", err)
|
||||
return
|
||||
}
|
||||
|
||||
var options []string
|
||||
for _, c := range clients {
|
||||
options = append(options, c["name"].(string))
|
||||
}
|
||||
|
||||
clientName, _ := pterm.DefaultInteractiveSelect.WithOptions(options).Show()
|
||||
|
||||
var selectedClient map[string]any
|
||||
for _, c := range clients {
|
||||
if c["name"].(string) == clientName {
|
||||
selectedClient = c
|
||||
client = int(c["id"].(float64))
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
options = []string{}
|
||||
for _, s := range selectedClient["sites"].([]interface{}) {
|
||||
options = append(options, s.(map[string]interface{})["name"].(string))
|
||||
}
|
||||
|
||||
siteName, _ := pterm.DefaultInteractiveSelect.WithOptions(options).Show()
|
||||
|
||||
for _, s := range selectedClient["sites"].([]interface{}) {
|
||||
if s.(map[string]interface{})["name"].(string) == siteName {
|
||||
site = int(s.(map[string]interface{})["id"].(float64))
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
script, err := rmm.GenerateInstallerScript(client, site, agentType)
|
||||
if err != nil {
|
||||
pterm.Error.Println("Failed to retrieve installer script:", err)
|
||||
return
|
||||
}
|
||||
//pterm.Info.Println("Script: ", script)
|
||||
|
||||
f, err := os.Create( "/tmp/trmm-installer.sh")
|
||||
if err != nil {
|
||||
pterm.Error.Println("Failed to create installer script on filesystem:", err)
|
||||
return
|
||||
}
|
||||
|
||||
defer f.Close()
|
||||
|
||||
_, err = f.WriteString(script)
|
||||
if err != nil {
|
||||
pterm.Error.Println("Failed to write installer script to filesystem:", err)
|
||||
return
|
||||
}
|
||||
|
||||
pterm.Info.Println("Installer script written to /tmp/trmm-installer.sh")
|
||||
|
||||
// check if running as root, if so, run the installer script
|
||||
usr, _ := user.Current()
|
||||
if usr.Uid == "0" && force {
|
||||
pterm.Info.Println("Running installer script...")
|
||||
exec.Command("bash", "/tmp/trmm-installer.sh")
|
||||
|
||||
} else {
|
||||
pterm.Info.Println("Run the installer script as root (or with sudo) to install the agent")
|
||||
pterm.Info.Println("Command: sudo bash /tmp/trmm-installer.sh")
|
||||
}
|
||||
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(installCmd)
|
||||
|
||||
installCmd.Flags().StringP("api-key", "a", "", "API key for the Tactical RMM Server")
|
||||
installCmd.Flags().StringP("url", "u", "", "URL for the Tactical RMM API Server")
|
||||
installCmd.Flags().IntP("client", "c", -1, "Client ID")
|
||||
installCmd.Flags().IntP("site", "s", -1, "Site ID")
|
||||
installCmd.Flags().StringP("type", "t", "server", "Agent Type (can be server or workstation)")
|
||||
|
||||
installCmd.Flags().BoolP("force", "f", false, "Don't prompt to run installer script")
|
||||
|
||||
installCmd.MarkPersistentFlagRequired("api-key")
|
||||
installCmd.MarkPersistentFlagRequired("url")
|
||||
}
|
||||
52
cmd/root.go
Normal file
52
cmd/root.go
Normal file
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
Copyright © 2024 NAME HERE <EMAIL ADDRESS>
|
||||
*/
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/pterm/pterm"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// rootCmd represents the base command when called without any subcommands
|
||||
var rootCmd = &cobra.Command{
|
||||
Use: "trmm-linux-installer",
|
||||
Short: "Installs the Tactical RMM Agent on Linux",
|
||||
Long: ``,
|
||||
//PersistentPreRun: func(cmd *cobra.Command, args []string) {
|
||||
//},
|
||||
|
||||
// Uncomment the following line if your bare application
|
||||
// has an action associated with it:
|
||||
// Run: func(cmd *cobra.Command, args []string) { },
|
||||
}
|
||||
|
||||
// Execute adds all child commands to the root command and sets flags appropriately.
|
||||
// This is called by main.main(). It only needs to happen once to the rootCmd.
|
||||
func Execute() {
|
||||
err := rootCmd.Execute()
|
||||
if err != nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
// Here you will define your flags and configuration settings.
|
||||
// Cobra supports persistent flags, which, if defined here,
|
||||
// will be global for your application.
|
||||
|
||||
// Cobra also supports local flags, which will only run
|
||||
// when this action is called directly.
|
||||
//rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
|
||||
|
||||
rootCmd.PersistentFlags().StringP("agent-download-url", "D", "https://github.com/soarinferret/rmmagent-builder/", "Manually specify the agent download URL")
|
||||
}
|
||||
|
||||
func pExit(s string, err error) {
|
||||
if err != nil {
|
||||
pterm.Error.Println(s, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
130
cmd/update.go
Normal file
130
cmd/update.go
Normal file
@@ -0,0 +1,130 @@
|
||||
package cmd
|
||||
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"io"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/pterm/pterm"
|
||||
"github.com/soarinferret/trmm-lam/internal/tacticalrmm"
|
||||
)
|
||||
|
||||
// https://stackoverflow.com/a/33853856/13335339
|
||||
func downloadFile(filepath string, url string) (err error) {
|
||||
// Create the file
|
||||
out, err := os.Create(filepath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer out.Close()
|
||||
|
||||
// Get the data
|
||||
resp, err := http.Get(url)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// Check server response
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("bad status: %s", resp.Status)
|
||||
}
|
||||
|
||||
// Writer the body to file
|
||||
_, err = io.Copy(out, resp.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var updateCmd = &cobra.Command{
|
||||
Use: "update",
|
||||
Aliases: []string{"u"},
|
||||
Short: "Update TRMM Agent on Linux",
|
||||
Long: ``,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
agentDlUrl, _ := cmd.Flags().GetString("agent-download-url")
|
||||
|
||||
rmm := tacticalrmm.New("", "", agentDlUrl)
|
||||
|
||||
url, err := rmm.GetAgentDownloadUrl()
|
||||
if err != nil {
|
||||
pterm.Error.Println("Failed to retrieve agent download URL:", err)
|
||||
return
|
||||
}
|
||||
|
||||
agentName := "tacticalagent"
|
||||
|
||||
fname, err := exec.LookPath(agentName)
|
||||
if err == nil {
|
||||
fname, _ = filepath.Abs(fname)
|
||||
}
|
||||
if err != nil {
|
||||
pterm.Error.Println("Failed to find agent binary:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// get the current agent version
|
||||
v := exec.Command(fname, "version")
|
||||
out, err := v.Output()
|
||||
if err != nil {
|
||||
pterm.Error.Println("Failed to get agent version:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
//out = strings.TrimSpace(out)
|
||||
|
||||
pterm.Info.Println("Current agent version:", string(out))
|
||||
|
||||
// Latest available version
|
||||
latest, err := rmm.GetLatestAgentVersion()
|
||||
if err != nil {
|
||||
pterm.Error.Println("Failed to get latest agent version:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
pterm.Info.Println("Latest agent version:", latest)
|
||||
|
||||
if strings.Contains(latest, strings.TrimSpace(string(out))) {
|
||||
pterm.Info.Println("Agent is already up to date")
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
// move the old agent to a backup
|
||||
err = os.Rename(fname, fname+".old")
|
||||
|
||||
// replace the agent
|
||||
err = downloadFile(fname, url)
|
||||
if err != nil {
|
||||
pterm.Error.Println("Failed to download agent:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
// make the agent executable
|
||||
err = os.Chmod(fname, 0755)
|
||||
if err != nil {
|
||||
pterm.Error.Println("Failed to make agent executable:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// restart the service
|
||||
c := exec.Command("systemctl", "restart", "tacticalagent")
|
||||
err = c.Run()
|
||||
if err != nil {
|
||||
pterm.Error.Println("Failed to restart agent service:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
pterm.Info.Println("Agent updated successfully")
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(updateCmd)
|
||||
}
|
||||
21
devbox.json
Normal file
21
devbox.json
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/jetify-com/devbox/0.10.7/.schema/devbox.schema.json",
|
||||
"packages": [
|
||||
"go@1.22.6"
|
||||
],
|
||||
"env": {
|
||||
"GOPATH": "$HOME/go/",
|
||||
"PATH": "$PATH:$HOME/go/bin"
|
||||
},
|
||||
"shell": {
|
||||
"init_hook": [
|
||||
"export \"GOROOT=$(go env GOROOT)\""
|
||||
],
|
||||
"scripts": {
|
||||
"test": [
|
||||
"echo \"Error: no test specified\" && exit 1"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
53
devbox.lock
Normal file
53
devbox.lock
Normal file
@@ -0,0 +1,53 @@
|
||||
{
|
||||
"lockfile_version": "1",
|
||||
"packages": {
|
||||
"go@1.22.6": {
|
||||
"last_modified": "2024-08-31T10:12:23Z",
|
||||
"resolved": "github:NixOS/nixpkgs/5629520edecb69630a3f4d17d3d33fc96c13f6fe#go",
|
||||
"source": "devbox-search",
|
||||
"version": "1.22.6",
|
||||
"systems": {
|
||||
"aarch64-darwin": {
|
||||
"outputs": [
|
||||
{
|
||||
"name": "out",
|
||||
"path": "/nix/store/bman2jjx2ykfclj3g0wb89cxyzqygh8y-go-1.22.6",
|
||||
"default": true
|
||||
}
|
||||
],
|
||||
"store_path": "/nix/store/bman2jjx2ykfclj3g0wb89cxyzqygh8y-go-1.22.6"
|
||||
},
|
||||
"aarch64-linux": {
|
||||
"outputs": [
|
||||
{
|
||||
"name": "out",
|
||||
"path": "/nix/store/gnm672jywl1b778ql6pf57xka45452b6-go-1.22.6",
|
||||
"default": true
|
||||
}
|
||||
],
|
||||
"store_path": "/nix/store/gnm672jywl1b778ql6pf57xka45452b6-go-1.22.6"
|
||||
},
|
||||
"x86_64-darwin": {
|
||||
"outputs": [
|
||||
{
|
||||
"name": "out",
|
||||
"path": "/nix/store/qvr3slzx5av20xkw6i97yz7wla9sf4nc-go-1.22.6",
|
||||
"default": true
|
||||
}
|
||||
],
|
||||
"store_path": "/nix/store/qvr3slzx5av20xkw6i97yz7wla9sf4nc-go-1.22.6"
|
||||
},
|
||||
"x86_64-linux": {
|
||||
"outputs": [
|
||||
{
|
||||
"name": "out",
|
||||
"path": "/nix/store/6rybf4g5b77kz27k07avr7qd44ssw3l2-go-1.22.6",
|
||||
"default": true
|
||||
}
|
||||
],
|
||||
"store_path": "/nix/store/6rybf4g5b77kz27k07avr7qd44ssw3l2-go-1.22.6"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
26
go.mod
Normal file
26
go.mod
Normal file
@@ -0,0 +1,26 @@
|
||||
module github.com/soarinferret/trmm-lam
|
||||
|
||||
go 1.22.6
|
||||
|
||||
require (
|
||||
github.com/gorilla/websocket v1.5.3
|
||||
github.com/pterm/pterm v0.12.80
|
||||
github.com/spf13/cobra v1.9.1
|
||||
)
|
||||
|
||||
require (
|
||||
atomicgo.dev/cursor v0.2.0 // indirect
|
||||
atomicgo.dev/keyboard v0.2.9 // indirect
|
||||
atomicgo.dev/schedule v0.1.0 // indirect
|
||||
github.com/containerd/console v1.0.3 // indirect
|
||||
github.com/gookit/color v1.5.4 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/lithammer/fuzzysearch v1.1.8 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.16 // indirect
|
||||
github.com/rivo/uniseg v0.4.4 // indirect
|
||||
github.com/spf13/pflag v1.0.6 // indirect
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||
golang.org/x/sys v0.27.0 // indirect
|
||||
golang.org/x/term v0.26.0 // indirect
|
||||
golang.org/x/text v0.20.0 // indirect
|
||||
)
|
||||
128
go.sum
Normal file
128
go.sum
Normal file
@@ -0,0 +1,128 @@
|
||||
atomicgo.dev/assert v0.0.2 h1:FiKeMiZSgRrZsPo9qn/7vmr7mCsh5SZyXY4YGYiYwrg=
|
||||
atomicgo.dev/assert v0.0.2/go.mod h1:ut4NcI3QDdJtlmAxQULOmA13Gz6e2DWbSAS8RUOmNYQ=
|
||||
atomicgo.dev/cursor v0.2.0 h1:H6XN5alUJ52FZZUkI7AlJbUc1aW38GWZalpYRPpoPOw=
|
||||
atomicgo.dev/cursor v0.2.0/go.mod h1:Lr4ZJB3U7DfPPOkbH7/6TOtJ4vFGHlgj1nc+n900IpU=
|
||||
atomicgo.dev/keyboard v0.2.9 h1:tOsIid3nlPLZ3lwgG8KZMp/SFmr7P0ssEN5JUsm78K8=
|
||||
atomicgo.dev/keyboard v0.2.9/go.mod h1:BC4w9g00XkxH/f1HXhW2sXmJFOCWbKn9xrOunSFtExQ=
|
||||
atomicgo.dev/schedule v0.1.0 h1:nTthAbhZS5YZmgYbb2+DH8uQIZcTlIrd4eYr3UQxEjs=
|
||||
atomicgo.dev/schedule v0.1.0/go.mod h1:xeUa3oAkiuHYh8bKiQBRojqAMq3PXXbJujjb0hw8pEU=
|
||||
github.com/MarvinJWendt/testza v0.1.0/go.mod h1:7AxNvlfeHP7Z/hDQ5JtE3OKYT3XFUeLCDE2DQninSqs=
|
||||
github.com/MarvinJWendt/testza v0.2.1/go.mod h1:God7bhG8n6uQxwdScay+gjm9/LnO4D3kkcZX4hv9Rp8=
|
||||
github.com/MarvinJWendt/testza v0.2.8/go.mod h1:nwIcjmr0Zz+Rcwfh3/4UhBp7ePKVhuBExvZqnKYWlII=
|
||||
github.com/MarvinJWendt/testza v0.2.10/go.mod h1:pd+VWsoGUiFtq+hRKSU1Bktnn+DMCSrDrXDpX2bG66k=
|
||||
github.com/MarvinJWendt/testza v0.2.12/go.mod h1:JOIegYyV7rX+7VZ9r77L/eH6CfJHHzXjB69adAhzZkI=
|
||||
github.com/MarvinJWendt/testza v0.3.0/go.mod h1:eFcL4I0idjtIx8P9C6KkAuLgATNKpX4/2oUqKc6bF2c=
|
||||
github.com/MarvinJWendt/testza v0.4.2/go.mod h1:mSdhXiKH8sg/gQehJ63bINcCKp7RtYewEjXsvsVUPbE=
|
||||
github.com/MarvinJWendt/testza v0.5.2 h1:53KDo64C1z/h/d/stCYCPY69bt/OSwjq5KpFNwi+zB4=
|
||||
github.com/MarvinJWendt/testza v0.5.2/go.mod h1:xu53QFE5sCdjtMCKk8YMQ2MnymimEctc4n3EjyIYvEY=
|
||||
github.com/atomicgo/cursor v0.0.1/go.mod h1:cBON2QmmrysudxNBFthvMtN32r3jxVRIvzkUiF/RuIk=
|
||||
github.com/containerd/console v1.0.3 h1:lIr7SlA5PxZyMV30bDW0MGbiOPXwc63yRuCP0ARubLw=
|
||||
github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/gookit/color v1.4.2/go.mod h1:fqRyamkC1W8uxl+lxCQxOT09l/vYfZ+QeiX3rKQHCoQ=
|
||||
github.com/gookit/color v1.5.0/go.mod h1:43aQb+Zerm/BWh2GnrgOQm7ffz7tvQXEKV6BFMl7wAo=
|
||||
github.com/gookit/color v1.5.4 h1:FZmqs7XOyGgCAxmWyPslpiok1k05wmY3SJTytgvYFs0=
|
||||
github.com/gookit/color v1.5.4/go.mod h1:pZJOeOS8DM43rXbp4AZo1n9zCU2qjpcRko0b6/QJi9w=
|
||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
github.com/klauspost/cpuid/v2 v2.0.10/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c=
|
||||
github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c=
|
||||
github.com/klauspost/cpuid/v2 v2.2.3 h1:sxCkb+qR91z4vsqw4vGGZlDgPz3G7gjaLyK3V8y70BU=
|
||||
github.com/klauspost/cpuid/v2 v2.2.3/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/lithammer/fuzzysearch v1.1.8 h1:/HIuJnjHuXS8bKaiTMeeDlW2/AyIWk2brx1V8LFgLN4=
|
||||
github.com/lithammer/fuzzysearch v1.1.8/go.mod h1:IdqeyBClc3FFqSzYq/MXESsS4S0FsZ5ajtkr5xPLts4=
|
||||
github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
||||
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pterm/pterm v0.12.27/go.mod h1:PhQ89w4i95rhgE+xedAoqous6K9X+r6aSOI2eFF7DZI=
|
||||
github.com/pterm/pterm v0.12.29/go.mod h1:WI3qxgvoQFFGKGjGnJR849gU0TsEOvKn5Q8LlY1U7lg=
|
||||
github.com/pterm/pterm v0.12.30/go.mod h1:MOqLIyMOgmTDz9yorcYbcw+HsgoZo3BQfg2wtl3HEFE=
|
||||
github.com/pterm/pterm v0.12.31/go.mod h1:32ZAWZVXD7ZfG0s8qqHXePte42kdz8ECtRyEejaWgXU=
|
||||
github.com/pterm/pterm v0.12.33/go.mod h1:x+h2uL+n7CP/rel9+bImHD5lF3nM9vJj80k9ybiiTTE=
|
||||
github.com/pterm/pterm v0.12.36/go.mod h1:NjiL09hFhT/vWjQHSj1athJpx6H8cjpHXNAK5bUw8T8=
|
||||
github.com/pterm/pterm v0.12.40/go.mod h1:ffwPLwlbXxP+rxT0GsgDTzS3y3rmpAO1NMjUkGTYf8s=
|
||||
github.com/pterm/pterm v0.12.80 h1:mM55B+GnKUnLMUSqhdINe4s6tOuVQIetQ3my8JGyAIg=
|
||||
github.com/pterm/pterm v0.12.80/go.mod h1:c6DeF9bSnOSeFPZlfs4ZRAFcf5SCoTwvwQ5xaKGQlHo=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis=
|
||||
github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ=
|
||||
github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
|
||||
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
|
||||
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
|
||||
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
|
||||
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs=
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E=
|
||||
golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211013075003-97ac67df715c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s=
|
||||
golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.26.0 h1:WEQa6V3Gja/BhNxg540hBip/kkaYtRg3cxg4oXSw4AU=
|
||||
golang.org/x/term v0.26.0/go.mod h1:Si5m1o57C5nBNQo5z1iq+XDijt21BDBDp2bK0QI8e3E=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug=
|
||||
golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
138
internal/meshcentral/ms.go
Normal file
138
internal/meshcentral/ms.go
Normal file
@@ -0,0 +1,138 @@
|
||||
package meshcentral
|
||||
|
||||
import (
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"time"
|
||||
"strings"
|
||||
|
||||
|
||||
"fmt"
|
||||
"context"
|
||||
"errors"
|
||||
"net/url"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
|
||||
type MeshResponse struct {
|
||||
Action string `json:"action"`
|
||||
Meshes []struct {
|
||||
Name string `json:"name"`
|
||||
ID string `json:"_id"`
|
||||
} `json:"meshes"`
|
||||
}
|
||||
|
||||
func getMeshDeviceGroupID(ctx context.Context, uri, deviceGroup string) (string, error) {
|
||||
conn, _, err := websocket.DefaultDialer.Dial(uri, nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
err = conn.WriteMessage(websocket.TextMessage, []byte(`{"action": "meshes", "responseid": "meshctrl"}`))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
for {
|
||||
_, message, err := conn.ReadMessage()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
var response MeshResponse
|
||||
if err := json.Unmarshal(message, &response); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if response.Action == "meshes" {
|
||||
for _, mesh := range response.Meshes {
|
||||
if mesh.Name == deviceGroup {
|
||||
return mesh.ID[len("mesh//"):], nil
|
||||
}
|
||||
}
|
||||
return "", errors.New("device group not found")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func GetMeshDeviceGroupId(uri string, deviceGroup string) (id string, err error) {
|
||||
ctx := context.Background()
|
||||
|
||||
id, err = getMeshDeviceGroupID(ctx, uri, deviceGroup)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
|
||||
return id, nil
|
||||
}
|
||||
|
||||
func formatUserID(user, domain string) string {
|
||||
return "user/" + domain + "/" + user
|
||||
}
|
||||
|
||||
func getAuthToken(user, key, domain string) (string, error) {
|
||||
keyBytes, err := hex.DecodeString(key)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if len(keyBytes) < 32 {
|
||||
return "", errors.New("key length must be at least 32 bytes")
|
||||
}
|
||||
key1 := keyBytes[:32]
|
||||
|
||||
msg := fmt.Sprintf(`{"userid":"%s", "domainid":"%s", "time":%d}`,
|
||||
formatUserID(user, domain), domain, time.Now().Unix())
|
||||
|
||||
//fmt.Println("msg: ", msg)
|
||||
//iv, err := hex.DecodeString("000000000000000000000000")
|
||||
iv := make([]byte, 12)
|
||||
_, err = rand.Read(iv)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
block, err := aes.NewCipher(key1)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
aesGCM, err := cipher.NewGCM(block)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Encrypt and generate authentication tag
|
||||
ciphertext := aesGCM.Seal(nil, iv, []byte(msg), nil)
|
||||
tag := ciphertext[len(ciphertext)-aesGCM.Overhead():]
|
||||
ciphertext = ciphertext[:len(ciphertext)-aesGCM.Overhead()]
|
||||
|
||||
// Concatenate IV, tag, and ciphertext
|
||||
data := append(iv, tag...)
|
||||
data = append(data, ciphertext...)
|
||||
|
||||
// Base64 encode and replace characters to match Python's altchars "@$"
|
||||
encoded := base64.StdEncoding.EncodeToString(data)
|
||||
encoded = strings.ReplaceAll(encoded, "/", "$")
|
||||
encoded = strings.ReplaceAll(encoded, "+", "@")
|
||||
|
||||
return encoded, nil
|
||||
}
|
||||
|
||||
|
||||
func GetMeshWsUrl(uri string, user string, token string) (string, error) {
|
||||
newToken, err := getAuthToken(user, token, "")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
u, _ := url.Parse(uri)
|
||||
return "wss://" + u.Host + "/control.ashx?auth=" + newToken, nil
|
||||
}
|
||||
237
internal/tacticalrmm/constants.go
Normal file
237
internal/tacticalrmm/constants.go
Normal file
@@ -0,0 +1,237 @@
|
||||
package tacticalrmm
|
||||
|
||||
// https://github.com/amidaware/tacticalrmm/blob/v1.0.0/api/tacticalrmm/core/agent_linux.sh
|
||||
// the following script is licensed under the "Tactical RMM License Version 1.0"
|
||||
const LINUX_INSTALL_SCRIPT = `#!/usr/bin/env bash
|
||||
if [ $EUID -ne 0 ]; then
|
||||
echo "ERROR: Must be run as root"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
HAS_SYSTEMD=$(ps --no-headers -o comm 1)
|
||||
if [ "${HAS_SYSTEMD}" != 'systemd' ]; then
|
||||
echo "This install script only supports systemd"
|
||||
echo "Please install systemd or manually create the service using your systems's service manager"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ $DISPLAY ]]; then
|
||||
echo "ERROR: Display detected. Installer only supports running headless, i.e from ssh."
|
||||
echo "If you cannot ssh in then please run 'sudo systemctl isolate multi-user.target' to switch to a non-graphical user session and run the installer again."
|
||||
echo "If you are already running headless, then you are probably running with X forwarding which is setting DISPLAY, if so then simply run"
|
||||
echo "unset DISPLAY"
|
||||
echo "to unset the variable and then try running the installer again"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
DEBUG=0
|
||||
INSECURE=0
|
||||
NOMESH=0
|
||||
|
||||
agentDL='agentDLChange'
|
||||
meshDL='meshDLChange'
|
||||
|
||||
apiURL='apiURLChange'
|
||||
token='tokenChange'
|
||||
clientID='clientIDChange'
|
||||
siteID='siteIDChange'
|
||||
agentType='agentTypeChange'
|
||||
proxy=''
|
||||
|
||||
agentBinPath='/usr/local/bin'
|
||||
binName='tacticalagent'
|
||||
agentBin="${agentBinPath}/${binName}"
|
||||
agentConf='/etc/tacticalagent'
|
||||
agentSvcName='tacticalagent.service'
|
||||
agentSysD="/etc/systemd/system/${agentSvcName}"
|
||||
agentDir='/opt/tacticalagent'
|
||||
meshDir='/opt/tacticalmesh'
|
||||
meshSystemBin="${meshDir}/meshagent"
|
||||
meshSvcName='meshagent.service'
|
||||
meshSysD="/lib/systemd/system/${meshSvcName}"
|
||||
|
||||
deb=(ubuntu debian raspbian kali linuxmint)
|
||||
rhe=(fedora rocky centos rhel amzn arch opensuse)
|
||||
|
||||
set_locale_deb() {
|
||||
locale-gen "en_US.UTF-8"
|
||||
localectl set-locale LANG=en_US.UTF-8
|
||||
. /etc/default/locale
|
||||
}
|
||||
|
||||
set_locale_rhel() {
|
||||
localedef -c -i en_US -f UTF-8 en_US.UTF-8 >/dev/null 2>&1
|
||||
localectl set-locale LANG=en_US.UTF-8
|
||||
. /etc/locale.conf
|
||||
}
|
||||
|
||||
RemoveOldAgent() {
|
||||
if [ -f "${agentSysD}" ]; then
|
||||
systemctl disable ${agentSvcName}
|
||||
systemctl stop ${agentSvcName}
|
||||
rm -f "${agentSysD}"
|
||||
systemctl daemon-reload
|
||||
fi
|
||||
|
||||
if [ -f "${agentConf}" ]; then
|
||||
rm -f "${agentConf}"
|
||||
fi
|
||||
|
||||
if [ -f "${agentBin}" ]; then
|
||||
rm -f "${agentBin}"
|
||||
fi
|
||||
|
||||
if [ -d "${agentDir}" ]; then
|
||||
rm -rf "${agentDir}"
|
||||
fi
|
||||
}
|
||||
|
||||
InstallMesh() {
|
||||
if [ -f /etc/os-release ]; then
|
||||
distroID=$(
|
||||
. /etc/os-release
|
||||
echo $ID
|
||||
)
|
||||
distroIDLIKE=$(
|
||||
. /etc/os-release
|
||||
echo $ID_LIKE
|
||||
)
|
||||
if [[ " ${deb[*]} " =~ " ${distroID} " ]]; then
|
||||
set_locale_deb
|
||||
elif [[ " ${deb[*]} " =~ " ${distroIDLIKE} " ]]; then
|
||||
set_locale_deb
|
||||
elif [[ " ${rhe[*]} " =~ " ${distroID} " ]]; then
|
||||
set_locale_rhel
|
||||
else
|
||||
set_locale_rhel
|
||||
fi
|
||||
fi
|
||||
|
||||
meshTmpDir='/root/meshtemp'
|
||||
mkdir -p $meshTmpDir
|
||||
|
||||
meshTmpBin="${meshTmpDir}/meshagent"
|
||||
wget --no-check-certificate -q -O ${meshTmpBin} ${meshDL}
|
||||
chmod +x ${meshTmpBin}
|
||||
mkdir -p ${meshDir}
|
||||
env LC_ALL=en_US.UTF-8 LANGUAGE=en_US XAUTHORITY=foo DISPLAY=bar ${meshTmpBin} -install --installPath=${meshDir}
|
||||
sleep 1
|
||||
rm -rf ${meshTmpDir}
|
||||
}
|
||||
|
||||
RemoveMesh() {
|
||||
if [ -f "${meshSystemBin}" ]; then
|
||||
env XAUTHORITY=foo DISPLAY=bar ${meshSystemBin} -uninstall
|
||||
sleep 1
|
||||
fi
|
||||
|
||||
if [ -f "${meshSysD}" ]; then
|
||||
systemctl stop ${meshSvcName} >/dev/null 2>&1
|
||||
systemctl disable ${meshSvcName} >/dev/null 2>&1
|
||||
rm -f ${meshSysD}
|
||||
fi
|
||||
|
||||
rm -rf ${meshDir}
|
||||
systemctl daemon-reload
|
||||
}
|
||||
|
||||
Uninstall() {
|
||||
RemoveMesh
|
||||
RemoveOldAgent
|
||||
}
|
||||
|
||||
if [ $# -ne 0 ] && [[ $1 =~ ^(uninstall|-uninstall|--uninstall)$ ]]; then
|
||||
Uninstall
|
||||
# Remove the current script
|
||||
rm "$0"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
while [[ "$#" -gt 0 ]]; do
|
||||
case $1 in
|
||||
-debug | --debug | debug) DEBUG=1 ;;
|
||||
-insecure | --insecure | insecure) INSECURE=1 ;;
|
||||
-nomesh | --nomesh | nomesh) NOMESH=1 ;;
|
||||
*)
|
||||
echo "ERROR: Unknown parameter: $1"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
|
||||
RemoveOldAgent
|
||||
|
||||
echo "Downloading tactical agent..."
|
||||
wget -q -O ${agentBin} "${agentDL}"
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "ERROR: Unable to download tactical agent"
|
||||
exit 1
|
||||
fi
|
||||
chmod +x ${agentBin}
|
||||
|
||||
MESH_NODE_ID=""
|
||||
|
||||
if [[ $NOMESH -eq 1 ]]; then
|
||||
echo "Skipping mesh install"
|
||||
else
|
||||
if [ -f "${meshSystemBin}" ]; then
|
||||
RemoveMesh
|
||||
fi
|
||||
echo "Downloading and installing mesh agent..."
|
||||
InstallMesh
|
||||
sleep 2
|
||||
echo "Getting mesh node id..."
|
||||
MESH_NODE_ID=$(env XAUTHORITY=foo DISPLAY=bar ${agentBin} -m nixmeshnodeid)
|
||||
fi
|
||||
|
||||
if [ ! -d "${agentBinPath}" ]; then
|
||||
echo "Creating ${agentBinPath}"
|
||||
mkdir -p ${agentBinPath}
|
||||
fi
|
||||
|
||||
INSTALL_CMD="${agentBin} -m install -api ${apiURL} -client-id ${clientID} -site-id ${siteID} -agent-type ${agentType} -auth ${token}"
|
||||
|
||||
if [ "${MESH_NODE_ID}" != '' ]; then
|
||||
INSTALL_CMD+=" --meshnodeid ${MESH_NODE_ID}"
|
||||
fi
|
||||
|
||||
if [[ $DEBUG -eq 1 ]]; then
|
||||
INSTALL_CMD+=" --log debug"
|
||||
fi
|
||||
|
||||
if [[ $INSECURE -eq 1 ]]; then
|
||||
INSTALL_CMD+=" --insecure"
|
||||
fi
|
||||
|
||||
if [ "${proxy}" != '' ]; then
|
||||
INSTALL_CMD+=" --proxy ${proxy}"
|
||||
fi
|
||||
|
||||
eval ${INSTALL_CMD}
|
||||
|
||||
tacticalsvc="$(
|
||||
cat <<EOF
|
||||
[Unit]
|
||||
Description=Tactical RMM Linux Agent
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
ExecStart=${agentBin} -m svc
|
||||
User=root
|
||||
Group=root
|
||||
Restart=always
|
||||
RestartSec=5s
|
||||
LimitNOFILE=1000000
|
||||
KillMode=process
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
)"
|
||||
echo "${tacticalsvc}" | tee ${agentSysD} >/dev/null
|
||||
|
||||
systemctl daemon-reload
|
||||
systemctl enable ${agentSvcName}
|
||||
systemctl start ${agentSvcName}
|
||||
`
|
||||
301
internal/tacticalrmm/rmm.go
Normal file
301
internal/tacticalrmm/rmm.go
Normal file
@@ -0,0 +1,301 @@
|
||||
package tacticalrmm
|
||||
|
||||
import(
|
||||
"errors"
|
||||
"net/http"
|
||||
"io/ioutil"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/soarinferret/trmm-lam/internal/meshcentral"
|
||||
)
|
||||
|
||||
type TacticalRMM struct {
|
||||
url string
|
||||
apiKey string
|
||||
agentDownloadUrl string
|
||||
}
|
||||
|
||||
func New(url string, apiKey string, agentDl string) *TacticalRMM {
|
||||
return &TacticalRMM{
|
||||
url: url,
|
||||
apiKey: apiKey,
|
||||
agentDownloadUrl: agentDl,
|
||||
}
|
||||
}
|
||||
|
||||
func (rmm *TacticalRMM) ensureApiUrl() error {
|
||||
if rmm.url == "" {
|
||||
return errors.New("URL is not set")
|
||||
}
|
||||
if rmm.apiKey == "" {
|
||||
return errors.New("API Key is not set")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
func (rmm *TacticalRMM) get(url string) (response string, err error) {
|
||||
err = rmm.ensureApiUrl()
|
||||
if err != nil {
|
||||
return response, err
|
||||
}
|
||||
|
||||
// Create a new get request
|
||||
req, err := http.NewRequest("GET", rmm.url+url, nil)
|
||||
if err != nil {
|
||||
return response, err
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("X-API-KEY", rmm.apiKey)
|
||||
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return response, err
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return response, err
|
||||
}
|
||||
|
||||
return string(body), nil
|
||||
}
|
||||
|
||||
func (rmm *TacticalRMM) GetSettings() (settings map[string]any, err error) {
|
||||
|
||||
response, err := rmm.get("/core/settings/")
|
||||
if err != nil {
|
||||
return settings, err
|
||||
}
|
||||
|
||||
err = json.Unmarshal([]byte(response), &settings)
|
||||
if err != nil {
|
||||
return settings, err
|
||||
}
|
||||
|
||||
return settings, nil
|
||||
}
|
||||
|
||||
func (rmm *TacticalRMM) GetClients() (clients []map[string]any, err error) {
|
||||
|
||||
response, err := rmm.get("/clients/")
|
||||
if err != nil {
|
||||
return clients, err
|
||||
}
|
||||
|
||||
err = json.Unmarshal([]byte(response), &clients)
|
||||
if err != nil {
|
||||
return clients, err
|
||||
}
|
||||
|
||||
return clients, nil
|
||||
}
|
||||
|
||||
func (rmm *TacticalRMM) getMeshDownloadUrl() (url string, err error) {
|
||||
settings, err := rmm.GetSettings()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
token := settings["mesh_token"].(string)
|
||||
username := settings["mesh_username"].(string)
|
||||
mesh_site := settings["mesh_site"].(string)
|
||||
device_group := settings["mesh_device_group"].(string)
|
||||
|
||||
wsUrl, err := meshcentral.GetMeshWsUrl(mesh_site, username, token)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
id, err := meshcentral.GetMeshDeviceGroupId(wsUrl, device_group)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
meshUrl := mesh_site + "/meshagents?id=" + id + "&installflags=2&meshinstall=6"
|
||||
|
||||
return meshUrl, nil
|
||||
}
|
||||
|
||||
func parseGithubUrl(url string) (owner string, repo string, err error) {
|
||||
// should come in format of https://github.com/OWNER/REPO
|
||||
|
||||
// split url by '/'
|
||||
parts := strings.Split(url, "/")
|
||||
|
||||
// check if parts is less than 4, then return error
|
||||
// check if parts[2] is not equal to 'github.com', then return error
|
||||
// return parts[3] and parts[4]
|
||||
if len(parts) < 4 {
|
||||
return "", "", errors.New("Invalid URL")
|
||||
}
|
||||
|
||||
if parts[2] != "github.com" {
|
||||
return "", "", errors.New("Invalid URL")
|
||||
}
|
||||
|
||||
return parts[3], parts[4], nil
|
||||
}
|
||||
|
||||
func (rmm *TacticalRMM) GetAgentToken(client int, site int) (token string, err error) {
|
||||
// generate a fake windows agent so we can get the token
|
||||
|
||||
data := fmt.Sprintf(`
|
||||
{"installMethod":"manual",
|
||||
"client":%d,
|
||||
"site":%d,
|
||||
"expires":1,
|
||||
"agenttype":"server",
|
||||
"power":0,
|
||||
"rdp":0,
|
||||
"ping":0,
|
||||
"goarch":"amd64",
|
||||
"api":"%s",
|
||||
"fileName":"rmm.exe",
|
||||
"plat":"windows"}`, client, site, rmm.url )
|
||||
|
||||
reader := strings.NewReader(data)
|
||||
|
||||
req, err := http.NewRequest("POST", rmm.url+"/agents/installer/", reader)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("X-API-KEY", rmm.apiKey)
|
||||
|
||||
httpClient := &http.Client{}
|
||||
resp, err := httpClient.Do(req)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var response map[string]any
|
||||
err = json.NewDecoder(resp.Body).Decode(&response)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// parse token from the cmd
|
||||
cmd := response["cmd"].(string)
|
||||
parts := strings.Split(cmd, "--auth ")
|
||||
|
||||
return parts[1], nil
|
||||
}
|
||||
|
||||
func (rmm *TacticalRMM) GetAgentDownloadUrl() (url string, err error) {
|
||||
type Release struct {
|
||||
TagName string `json:"tag_name"`
|
||||
}
|
||||
|
||||
// check url does not contain 'github.com', then return the url
|
||||
if rmm.agentDownloadUrl != "" && !strings.Contains(rmm.agentDownloadUrl, "github.com") {
|
||||
return rmm.agentDownloadUrl, nil
|
||||
}
|
||||
|
||||
// parse url to get owner and repo
|
||||
owner, repo, err := parseGithubUrl(rmm.agentDownloadUrl)
|
||||
|
||||
// query api.github.com for latest release
|
||||
ghApiUrl := fmt.Sprintf("https://api.github.com/repos/%s/%s/releases/latest", owner, repo)
|
||||
|
||||
resp, err := http.Get(ghApiUrl)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return "", fmt.Errorf("GitHub API returned status: %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
var release Release
|
||||
err = json.NewDecoder(resp.Body).Decode(&release)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// https://github.com/SoarinFerret/rmmagent-builder/releases/download/v2.9.0/rmmagent-linux-amd64
|
||||
return fmt.Sprintf("https://github.com/%s/%s/releases/download/%s/rmmagent-linux-amd64", owner, repo, release.TagName), nil
|
||||
}
|
||||
|
||||
func (rmm *TacticalRMM) GetLatestAgentVersion() (version string, err error) {
|
||||
type Release struct {
|
||||
TagName string `json:"tag_name"`
|
||||
}
|
||||
|
||||
agentUrl := rmm.agentDownloadUrl
|
||||
if rmm.agentDownloadUrl != "" && !strings.Contains(rmm.agentDownloadUrl, "github.com") {
|
||||
agentUrl = "https://github.com/amidaware/rmmagent"
|
||||
}
|
||||
|
||||
owner, repo, err := parseGithubUrl(agentUrl)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
ghApiUrl := fmt.Sprintf("https://api.github.com/repos/%s/%s/releases/latest", owner, repo)
|
||||
|
||||
resp, err := http.Get(ghApiUrl)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return "", fmt.Errorf("GitHub API returned status: %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
var release Release
|
||||
err = json.NewDecoder(resp.Body).Decode(&release)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return release.TagName, nil
|
||||
}
|
||||
|
||||
func (rmm *TacticalRMM) GenerateInstallerScript(client int, site int, agentType string) (script string, err error) {
|
||||
|
||||
// we need the following
|
||||
// agentDL
|
||||
// meshDL
|
||||
// apiURL
|
||||
// token
|
||||
// clientID
|
||||
// siteID
|
||||
// agentType
|
||||
|
||||
agentDL, err := rmm.GetAgentDownloadUrl()
|
||||
if err != nil {
|
||||
return script, err
|
||||
}
|
||||
|
||||
meshDL, err := rmm.getMeshDownloadUrl()
|
||||
if err != nil {
|
||||
return script, err
|
||||
}
|
||||
|
||||
token, err := rmm.GetAgentToken(client, site)
|
||||
if err != nil {
|
||||
return script, err
|
||||
}
|
||||
|
||||
script = LINUX_INSTALL_SCRIPT
|
||||
script = strings.ReplaceAll(script, "agentDLChange", agentDL)
|
||||
script = strings.ReplaceAll(script, "meshDLChange", meshDL)
|
||||
script = strings.ReplaceAll(script, "apiURLChange", rmm.url)
|
||||
script = strings.ReplaceAll(script, "tokenChange", token)
|
||||
script = strings.ReplaceAll(script, "clientIDChange", fmt.Sprintf("%d", client))
|
||||
script = strings.ReplaceAll(script, "siteIDChange", fmt.Sprintf("%d", site))
|
||||
script = strings.ReplaceAll(script, "agentTypeChange", agentType)
|
||||
|
||||
return script, nil
|
||||
}
|
||||
15
main.go
Normal file
15
main.go
Normal file
@@ -0,0 +1,15 @@
|
||||
/*
|
||||
Copyright © 2024 Cody Ernesti
|
||||
*/
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/soarinferret/trmm-lam/cmd"
|
||||
)
|
||||
|
||||
var Version string
|
||||
|
||||
func main() {
|
||||
|
||||
cmd.Execute()
|
||||
}
|
||||
37
readme.md
Normal file
37
readme.md
Normal file
@@ -0,0 +1,37 @@
|
||||
# TacticalRMM Linux Agent Manager
|
||||
|
||||
Or trmm-lam for short.
|
||||
|
||||
## _What does this do?_
|
||||
|
||||
It manages your Tactical RMM agent on Linux systems. It can perform new installs in addition to updating existing installs.
|
||||
|
||||
For installs, it essentially just logs into your Tactical RMM server (and mesh central server), generates the installer script, and runs it. The installer script will install the agent on your system and configure it to communicate with your Tactical RMM server like usual.
|
||||
|
||||
For updates, it downloads the latest binary from my [GitHub releases page](https://github.com/SoarinFerret/rmmagent-builder) and replaces the existing binary with the new one. You can swap out the url with a commandline flag if you would like to use a different binary. All my binaries are built from the official source code (you can read my github actions), so you should be able to trust them.
|
||||
|
||||
## _Why did you make this?_
|
||||
|
||||
I wanted to install the TacticalRMM agent on my non-nixos Linux systems, and I wanted it to be as easy as possible. I also wanted to be able to easily update the agent when new versions are released.
|
||||
|
||||
## _How do I use this?_
|
||||
|
||||
You can download the latest release from the [releases page](https://github.com/soarinferret/trmm-lam/releases).
|
||||
|
||||
You can also build it yourself by cloning the repository and running `CGO_ENABLED=0 go build -ldflags="-s -w"`.
|
||||
|
||||
## _Does this require sponsorship or a license to use?_
|
||||
|
||||
No, this is using unofficial binaries and is not affiliated with Tactical RMM. You can use this without a license or sponsorship - but please consider sponsoring Amidaware / Tactical RMM if you are able to.
|
||||
|
||||
## _Is this installer official or affiliated with Amidaware LLC / Tactical RMM?_
|
||||
|
||||
Nope. This is a personal project that I made for my own use. I am not affiliated with Amidaware LLC or Tactical RMM in any way (besides being a paying customer through my employer).
|
||||
|
||||
## _Why is this written in golang instead of X scripting language?_
|
||||
|
||||
Because I can statically compile the binary and distribute it without worrying about dependencies. Also, I like writing small utilities in golang for fun.
|
||||
|
||||
## License
|
||||
|
||||
MIT licensed - see [LICENSE](LICENSE) for more information.
|
||||
Reference in New Issue
Block a user