This commit is contained in:
Soarinferret
2025-07-30 21:26:55 -05:00
commit 4ffe7462c7
14 changed files with 1287 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
.devbox/

21
LICENSE Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
}

View 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
View 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
View 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
View 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.