This commit is contained in:
wh1te909 2022-03-10 00:57:55 +00:00
parent c95d11da47
commit 202edc0588
91 changed files with 1431 additions and 860 deletions

View File

@ -1,11 +1,11 @@
# pulls community scripts from git repo
FROM python:3.9.9-slim AS GET_SCRIPTS_STAGE
FROM python:3.10-slim AS GET_SCRIPTS_STAGE
RUN apt-get update && \
apt-get install -y --no-install-recommends git && \
git clone https://github.com/amidaware/community-scripts.git /community-scripts
FROM python:3.9.9-slim
FROM python:3.10-slim
ENV TACTICAL_DIR /opt/tactical
ENV TACTICAL_READY_FILE ${TACTICAL_DIR}/tmp/tactical.ready
@ -17,6 +17,9 @@ ENV PYTHONUNBUFFERED=1
EXPOSE 8000 8383 8005
RUN apt-get update && \
apt-get install -y build-essential
RUN groupadd -g 1000 tactical && \
useradd -u 1000 -g 1000 tactical

View File

@ -133,6 +133,8 @@ if [ "$1" = 'tactical-init-dev' ]; then
# setup Python virtual env and install dependencies
! test -e "${VIRTUAL_ENV}" && python -m venv ${VIRTUAL_ENV}
"${VIRTUAL_ENV}"/bin/python -m pip install --upgrade pip
"${VIRTUAL_ENV}"/bin/pip install --no-cache-dir setuptools wheel
"${VIRTUAL_ENV}"/bin/pip install --no-cache-dir -r /requirements.txt
django_setup

View File

@ -1,37 +1,36 @@
# To ensure app dependencies are ported from your virtual environment/host machine into your container, run 'pip freeze > requirements.txt' in the terminal to overwrite this file
asyncio-nats-client
celery
channels
channels_redis
django-ipware
asgiref==3.5.0
celery==5.2.3
channels==3.0.4
channels_redis==3.3.1
daphne==3.0.2
Django==3.2.12
django-cors-headers
django-rest-knox
djangorestframework
msgpack
psycopg2-binary
pycparser
pycryptodome
pyotp
pyparsing
pytz
qrcode
redis
twilio
packaging
validators
websockets
black
Werkzeug
django-extensions
coverage
coveralls
model_bakery
mkdocs
mkdocs-material
pymdown-extensions
Pygments
pysnooper
isort
drf_spectacular
pandas
django-cors-headers==3.11.0
django-ipware==4.0.2
django-rest-knox==4.2.0
djangorestframework==3.13.1
future==0.18.2
msgpack==1.0.3
nats-py==2.0.0
packaging==21.3
psycopg2-binary==2.9.3
pycryptodome==3.14.1
pyotp==2.6.0
pytz==2021.3
qrcode==7.3.1
redis==4.1.3
requests==2.27.1
twilio==7.6.0
urllib3==1.26.8
validators==0.18.2
websockets==10.1
drf_spectacular==0.21.2
# dev
black==22.1.0
Werkzeug==2.0.2
django-extensions==3.1.5
Pygments==2.11.2
isort==5.10.1
mypy==0.931
types-pytz==2021.3.4

21
LICENSE
View File

@ -1,21 +0,0 @@
MIT License
Copyright (c) 2019-present wh1te909
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.

74
LICENSE.md Normal file
View File

@ -0,0 +1,74 @@
### Tactical RMM License Version 1.0
Text of license:&emsp;&emsp;&emsp;Copyright © 2022 AmidaWare LLC. All rights reserved.<br>
&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;&nbsp;Amending the text of this license is not permitted.
Trade Mark:&emsp;&emsp;&emsp;&emsp;"Tactical RMM" is a trade mark of AmidaWare LLC.
Licensor:&emsp;&emsp;&emsp;&emsp;&emsp;&nbsp;&nbsp;AmidaWare LLC of 1968 S Coast Hwy PMB 3847 Laguna Beach, CA, USA.
Licensed Software:&emsp;&nbsp;The software known as Tactical RMM Version v0.12.0 (and all subsequent releases and versions) and the Tactical RMM Agent v2.0.0 (and all subsequent releases and versions).
### 1. Preamble
The Licensed Software is designed to facilitate the remote monitoring and management (RMM) of networks, systems, servers, computers and other devices. The Licensed Software is made available primarily for use by organisations and managed service providers for monitoring and management purposes.
The Tactical RMM License is not an open-source software license. This license contains certain restrictions on the use of the Licensed Software. For example the functionality of the Licensed Software may not be made available as part of a SaaS (Software-as-a-Service) service or product to provide a commercial or for-profit service without the express prior permission of the Licensor.
### 2. License Grant
Permission is hereby granted, free of charge, on a non-exclusive basis, to copy, modify, create derivative works and use the Licensed Software in source and binary forms subject to the following terms and conditions. No additional rights will be implied under this license.
* The hosting and use of the Licensed Software to monitor and manage in-house networks/systems and/or customer networks/systems is permitted.
This license does not allow the functionality of the Licensed Software (whether in whole or in part) or a modified version of the Licensed Software or a derivative work to be used or otherwise made available as part of any other commercial or for-profit service, including, without limitation, any of the following:
* a service allowing third parties to interact remotely through a computer network;
* as part of a SaaS service or product;
* as part of the provision of a managed hosting service or product;
* the offering of installation and/or configuration services;
* the offer for sale, distribution or sale of any service or product (whether or not branded as Tactical RMM).
The prior written approval of AmidaWare LLC must be obtained for all commercial use and/or for-profit service use of the (i) Licensed Software (whether in whole or in part), (ii) a modified version of the Licensed Software and/or (iii) a derivative work.
The terms of this license apply to all copies of the Licensed Software (including modified versions) and derivative works.
All use of the Licensed Software must immediately cease if use breaches the terms of this license.
### 3. Derivative Works
If a derivative work is created which is based on or otherwise incorporates all or any part of the Licensed Software, and the derivative work is made available to any other person, the complete corresponding machine readable source code (including all changes made to the Licensed Software) must accompany the derivative work and be made publicly available online.
### 4. Copyright Notice
The following copyright notice shall be included in all copies of the Licensed Software:
&emsp;&emsp;&emsp;Copyright © 2022 AmidaWare LLC.
&emsp;&emsp;&emsp;Licensed under the Tactical RMM License Version 1.0 (the “License”).<br>
&emsp;&emsp;&emsp;You may only use the Licensed Software in accordance with the License.<br>
&emsp;&emsp;&emsp;A copy of the License is available at: https://license.tacticalrmm.com
### 5. Disclaimer of Warranty
THE LICENSED SOFTWARE IS PROVIDED "AS IS". TO THE FULLEST EXTENT PERMISSIBLE AT LAW ALL CONDITIONS, WARRANTIES OR OTHER TERMS OF ANY KIND WHICH MIGHT HAVE EFFECT OR BE IMPLIED OR INCORPORATED, WHETHER BY STATUTE, COMMON LAW OR OTHERWISE ARE HEREBY EXCLUDED, INCLUDING THE CONDITIONS, WARRANTIES OR OTHER TERMS AS TO SATISFACTORY QUALITY AND/OR MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, THE USE OF REASONABLE SKILL AND CARE AND NON-INFRINGEMENT.
### 6. Limits of Liability
THE FOLLOWING EXCLUSIONS SHALL APPLY TO THE FULLEST EXTENT PERMISSIBLE AT LAW. NEITHER THE AUTHORS NOR THE COPYRIGHT HOLDERS SHALL IN ANY CIRCUMSTANCES HAVE ANY LIABILITY FOR ANY CLAIM, LOSSES, DAMAGES OR OTHER LIABILITY, WHETHER THE SAME ARE SUFFERED DIRECTLY OR INDIRECTLY OR ARE IMMEDIATE OR CONSEQUENTIAL, AND WHETHER THE SAME ARISE IN CONTRACT, TORT OR DELICT (INCLUDING NEGLIGENCE) OR OTHERWISE HOWSOEVER ARISING FROM, OUT OF OR IN CONNECTION WITH THE LICENSED SOFTWARE OR THE USE OR INABILITY TO USE THE LICENSED SOFTWARE OR OTHER DEALINGS IN THE LICENSED SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH LOSS OR DAMAGE. THE FOREGOING EXCLUSIONS SHALL INCLUDE, WITHOUT LIMITATION, LIABILITY FOR ANY LOSSES OR DAMAGES WHICH FALL WITHIN ANY OF THE FOLLOWING CATEGORIES: SPECIAL, EXEMPLARY, OR INCIDENTAL LOSS OR DAMAGE, LOSS OF PROFITS, LOSS OF ANTICIPATED SAVINGS, LOSS OF BUSINESS OPPORTUNITY, LOSS OF GOODWILL, AND LOSS OR CORRUPTION OF DATA.
### 7. Termination
This license shall terminate with immediate effect if there is a material breach of any of its terms.
### 8. No partnership, agency or joint venture
Nothing in this license agreement is intended to, or shall be deemed to, establish any partnership or joint venture or any relationship of agency between AmidaWare LLC and any other person.
### 9. No endorsement
The names of the authors and/or the copyright holders must not be used to promote or endorse any products or services which are in any way derived from the Licensed Software without prior written consent.
### 10. Trademarks
No permission is granted to use the trademark “Tactical RMM” or any other trade name, trademark, service mark or product name of AmidaWare LLC except to the extent necessary to comply with the notice requirements in Section 4 (Copyright Notice).
### 11. Entire agreement
This license contains the whole agreement relating to its subject matter.
### 12. Severance
If any provision or part-provision of this license is or becomes invalid, illegal or unenforceable, it shall be deemed deleted, but that shall not affect the validity and enforceability of the rest of this license.
### 13. Acceptance of these terms
The terms and conditions of this license are accepted by copying, downloading, installing, redistributing, or otherwise using the Licensed Software.

View File

@ -32,6 +32,7 @@ agent = Recipe(
monitoring_type=cycle(["workstation", "server"]),
agent_id=seq(generate_agent_id("DESKTOP-TEST123")),
last_seen=djangotime.now() - djangotime.timedelta(days=5),
plat="windows"
)
server_agent = agent.extend(

View File

@ -5,7 +5,8 @@ from django.utils import timezone as djangotime
from packaging import version as pyver
from agents.models import Agent
from tacticalrmm.utils import AGENT_DEFER, reload_nats
from tacticalrmm.utils import reload_nats
from tacticalrmm.constants import AGENT_DEFER
class Command(BaseCommand):

View File

@ -168,7 +168,6 @@ class Command(BaseCommand):
public_ips = ["65.234.22.4", "74.123.43.5", "44.21.134.45"]
total_rams = [4, 8, 16, 32, 64, 128]
used_rams = [10, 13, 60, 25, 76, 34, 56, 34, 39]
now = dt.datetime.now()
@ -285,7 +284,6 @@ class Command(BaseCommand):
agent.hostname = random.choice(hostnames)
agent.version = settings.LATEST_AGENT_VER
agent.salt_ver = "1.1.0"
agent.site = Site.objects.get(name=site)
agent.agent_id = self.rand_string(25)
agent.description = random.choice(descriptions)
@ -295,10 +293,8 @@ class Command(BaseCommand):
agent.plat = "windows"
agent.plat_release = "windows-2019Server"
agent.total_ram = random.choice(total_rams)
agent.used_ram = random.choice(used_rams)
agent.boot_time = random.choice(boot_times)
agent.logged_in_username = random.choice(user_names)
agent.antivirus = "windowsdefender"
agent.mesh_node_id = (
"3UiLhe420@kaVQ0rswzBeonW$WY0xrFFUDBQlcYdXoriLXzvPmBpMrV99vRHXFlb"
)
@ -308,7 +304,6 @@ class Command(BaseCommand):
agent.wmi_detail = random.choice(wmi_details)
agent.services = services
agent.disks = random.choice(disks)
agent.salt_id = "not-used"
agent.save()
@ -329,9 +324,7 @@ class Command(BaseCommand):
agent=agent,
guid=i,
kb=windows_updates[i]["KBs"][0],
mandatory=windows_updates[i]["Mandatory"],
title=windows_updates[i]["Title"],
needs_reboot=windows_updates[i]["NeedsReboot"],
installed=windows_updates[i]["Installed"],
downloaded=windows_updates[i]["Downloaded"],
description=windows_updates[i]["Description"],

View File

@ -1,16 +0,0 @@
from django.core.management.base import BaseCommand
from agents.models import Agent
class Command(BaseCommand):
help = "Changes existing agents salt_id from a property to a model field"
def handle(self, *args, **kwargs):
agents = Agent.objects.filter(salt_id=None)
for agent in agents:
self.stdout.write(
self.style.SUCCESS(f"Setting salt_id on {agent.hostname}")
)
agent.salt_id = f"{agent.hostname}-{agent.pk}"
agent.save(update_fields=["salt_id"])

View File

@ -5,7 +5,7 @@ from packaging import version as pyver
from agents.models import Agent
from core.models import CoreSettings
from agents.tasks import send_agent_update_task
from tacticalrmm.utils import AGENT_DEFER
from tacticalrmm.constants import AGENT_DEFER
class Command(BaseCommand):

View File

@ -0,0 +1,25 @@
# Generated by Django 3.2.12 on 2022-02-27 05:54
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('agents', '0042_alter_agent_time_zone'),
]
operations = [
migrations.RemoveField(
model_name='agent',
name='antivirus',
),
migrations.RemoveField(
model_name='agent',
name='local_ip',
),
migrations.RemoveField(
model_name='agent',
name='used_ram',
),
]

View File

@ -0,0 +1,22 @@
# Generated by Django 3.2.12 on 2022-02-27 07:17
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('agents', '0043_auto_20220227_0554'),
]
operations = [
migrations.RenameField(
model_name='agent',
old_name='salt_id',
new_name='goarch',
),
migrations.RemoveField(
model_name='agent',
name='salt_ver',
),
]

View File

@ -30,24 +30,20 @@ class Agent(BaseAuditModel):
objects = PermissionQuerySet.as_manager()
version = models.CharField(default="0.1.0", max_length=255)
salt_ver = models.CharField(default="1.0.3", max_length=255)
operating_system = models.CharField(null=True, blank=True, max_length=255)
plat = models.CharField(max_length=255, null=True, blank=True)
goarch = models.CharField(max_length=255, null=True, blank=True)
plat_release = models.CharField(max_length=255, null=True, blank=True)
hostname = models.CharField(max_length=255)
salt_id = models.CharField(null=True, blank=True, max_length=255)
local_ip = models.TextField(null=True, blank=True) # deprecated
agent_id = models.CharField(max_length=200, unique=True)
last_seen = models.DateTimeField(null=True, blank=True)
services = models.JSONField(null=True, blank=True)
public_ip = models.CharField(null=True, max_length=255)
total_ram = models.IntegerField(null=True, blank=True)
used_ram = models.IntegerField(null=True, blank=True) # deprecated
disks = models.JSONField(null=True, blank=True)
boot_time = models.FloatField(null=True, blank=True)
logged_in_username = models.CharField(null=True, blank=True, max_length=255)
last_logged_in_user = models.CharField(null=True, blank=True, max_length=255)
antivirus = models.CharField(default="n/a", max_length=255) # deprecated
monitoring_type = models.CharField(max_length=30)
description = models.CharField(null=True, blank=True, max_length=255)
mesh_node_id = models.CharField(null=True, blank=True, max_length=255)
@ -91,8 +87,6 @@ class Agent(BaseAuditModel):
)
def save(self, *args, **kwargs):
from automation.tasks import generate_agent_checks_task
# get old agent if exists
old_agent = Agent.objects.get(pk=self.pk) if self.pk else None
super(Agent, self).save(old_model=old_agent, *args, **kwargs)
@ -108,6 +102,8 @@ class Agent(BaseAuditModel):
or (old_agent.monitoring_type != self.monitoring_type)
or (old_agent.block_policy_inheritance != self.block_policy_inheritance)
):
from automation.tasks import generate_agent_checks_task
generate_agent_checks_task.delay(agents=[self.pk], create_tasks=True)
def __str__(self):
@ -129,6 +125,9 @@ class Agent(BaseAuditModel):
@property
def arch(self):
if self.plat != "windows":
return self.goarch
if self.operating_system is not None:
if "64 bit" in self.operating_system or "64bit" in self.operating_system:
return "64"
@ -196,6 +195,12 @@ class Agent(BaseAuditModel):
@property
def cpu_model(self):
if self.plat == "linux":
try:
return self.wmi_detail["cpus"]
except:
return ["unknown cpu model"]
ret = []
try:
cpus = self.wmi_detail["cpu"]
@ -207,6 +212,14 @@ class Agent(BaseAuditModel):
@property
def graphics(self):
if self.plat == "linux":
try:
if not self.wmi_detail["gpus"]:
return "No graphics cards"
return self.wmi_detail["gpus"]
except:
return "Error getting graphics cards"
ret, mrda = [], []
try:
graphics = self.wmi_detail["graphics"]
@ -228,6 +241,12 @@ class Agent(BaseAuditModel):
@property
def local_ips(self):
if self.plat == "linux":
try:
return ", ".join(self.wmi_detail["local_ips"])
except:
return "error getting local ips"
ret = []
try:
ips = self.wmi_detail["network_config"]
@ -254,6 +273,12 @@ class Agent(BaseAuditModel):
@property
def make_model(self):
if self.plat == "linux":
try:
return self.wmi_detail["make_model"]
except:
return "error getting make/model"
try:
comp_sys = self.wmi_detail["comp_sys"][0]
comp_sys_prod = self.wmi_detail["comp_sys_prod"][0]
@ -284,6 +309,12 @@ class Agent(BaseAuditModel):
@property
def physical_disks(self):
if self.plat == "linux":
try:
return self.wmi_detail["disks"]
except:
return ["unknown disk"]
try:
disks = self.wmi_detail["disk"]
ret = []
@ -305,6 +336,42 @@ class Agent(BaseAuditModel):
except:
return ["unknown disk"]
def is_supported_script(self, shell: str) -> bool:
if self.plat.lower() == "windows" and shell in ["cmd", "powershell", "python"]:
return True
elif self.plat.lower() == "linux" and shell in ["shell", "python"]:
return True
else:
return False
def get_agent_policies(self):
site_policy = getattr(self.site, f"{self.monitoring_type}_policy", None)
client_policy = getattr(self.client, f"{self.monitoring_type}_policy", None)
default_policy = getattr(
CoreSettings.objects.first(), f"{self.monitoring_type}_policy", None
)
return {
"agent_policy": self.policy
if self.policy and not self.policy.is_agent_excluded(self)
else None,
"site_policy": site_policy
if (site_policy and not site_policy.is_agent_excluded(self))
and not self.block_policy_inheritance
else None,
"client_policy": client_policy
if (client_policy and not client_policy.is_agent_excluded(self))
and not self.block_policy_inheritance
and not self.site.block_policy_inheritance
else None,
"default_policy": default_policy
if (default_policy and not default_policy.is_agent_excluded(self))
and not self.block_policy_inheritance
and not self.site.block_policy_inheritance
and not self.client.block_policy_inheritance
else None,
}
def check_run_interval(self) -> int:
interval = self.check_interval
# determine if any agent checks have a custom interval and set the lowest interval

View File

@ -116,6 +116,8 @@ class AgentTableSerializer(serializers.ModelSerializer):
"italic",
"policy",
"block_policy_inheritance",
"plat",
"goarch",
]
depth = 2

View File

@ -13,7 +13,7 @@ from scripts.models import Script
from tacticalrmm.celery import app
from agents.models import Agent
from agents.utils import get_winagent_url
from agents.utils import get_agent_url
def agent_update(agent_id: str, force: bool = False) -> str:
@ -34,7 +34,7 @@ def agent_update(agent_id: str, force: bool = False) -> str:
version = settings.LATEST_AGENT_VER
inno = agent.win_inno_exe
url = get_winagent_url(agent.arch)
url = get_agent_url(agent.arch, agent.plat)
if not force:
if agent.pendingactions.filter(

View File

@ -1428,7 +1428,7 @@ class TestAgentTasks(TacticalTestCase):
self.authenticate()
self.setup_coresettings()
@patch("agents.utils.get_winagent_url")
@patch("agents.utils.get_agent_url")
@patch("agents.models.Agent.nats_cmd")
def test_agent_update(self, nats_cmd, get_url):
get_url.return_value = "https://exe.tacticalrmm.io"

View File

@ -40,5 +40,4 @@ urlpatterns = [
path("versions/", views.get_agent_versions),
path("update/", views.update_agents),
path("installer/", views.install_agent),
path("<str:arch>/getmeshexe/", views.get_mesh_exe),
]

View File

@ -1,33 +1,28 @@
import random
import asyncio
import urllib.parse
import requests
import tempfile
from django.conf import settings
from core.models import CodeSignToken
from django.http import FileResponse
from core.models import CoreSettings, CodeSignToken
from tacticalrmm.constants import MeshAgentIdent
from core.utils import get_mesh_ws_url, get_mesh_device_id
def get_exegen_url() -> str:
urls: list[str] = settings.EXE_GEN_URLS
for url in urls:
try:
r = requests.get(url, timeout=10)
except:
continue
def get_agent_url(arch: str, plat: str) -> str:
if r.status_code == 200:
return url
return random.choice(urls)
def get_winagent_url(arch: str) -> str:
dl_url = settings.DL_32 if arch == "32" else settings.DL_64
if plat == "windows":
endpoint = "winagents"
dl_url = settings.DL_32 if arch == "32" else settings.DL_64
else:
endpoint = "linuxagents"
dl_url = ""
try:
t: CodeSignToken = CodeSignToken.objects.first() # type: ignore
if t.is_valid:
base_url = get_exegen_url() + "/api/v1/winagents/?"
base_url = settings.EXE_GEN_URL + f"/api/v1/{endpoint}/?"
params = {
"version": settings.LATEST_AGENT_VER,
"arch": arch,
@ -38,3 +33,56 @@ def get_winagent_url(arch: str) -> str:
pass
return dl_url
def generate_linux_install(
client: str,
site: str,
agent_type: str,
arch: str,
token: str,
api: str,
download_url: str,
) -> FileResponse:
match arch:
case "amd64":
arch_id = MeshAgentIdent.LINUX64
case "386":
arch_id = MeshAgentIdent.LINUX32
case "arm64":
arch_id = MeshAgentIdent.LINUX_ARM_64
case "arm":
arch_id = MeshAgentIdent.LINUX_ARM_HF
core: CoreSettings = CoreSettings.objects.first() # type: ignore
uri = get_mesh_ws_url()
mesh_id = asyncio.run(get_mesh_device_id(uri, core.mesh_device_group))
mesh_dl = f"{core.mesh_site}/meshagents?id={mesh_id}&installflags=0&meshinstall={arch_id}" # type: ignore
sh = settings.LINUX_AGENT_SCRIPT
with open(sh, "r") as f:
text = f.read()
replace = {
"agentDLChange": download_url,
"meshDLChange": mesh_dl,
"clientIDChange": client,
"siteIDChange": site,
"agentTypeChange": agent_type,
"tokenChange": token,
"apiURLChange": api,
}
for i, j in replace.items():
text = text.replace(i, j)
with tempfile.NamedTemporaryFile() as fp:
with open(fp.name, "w") as f:
f.write(text)
f.write("\n")
return FileResponse(
open(fp.name, "rb"), as_attachment=True, filename="linux_agent_install.sh"
)

View File

@ -17,7 +17,7 @@ from rest_framework.response import Response
from rest_framework.views import APIView
from rest_framework.exceptions import PermissionDenied
from core.models import CoreSettings
from core.models import CoreSettings, CodeSignToken
from logs.models import AuditLog, DebugLog, PendingAction
from scripts.models import Script
from scripts.tasks import handle_bulk_command_task, handle_bulk_script_task
@ -25,8 +25,9 @@ from tacticalrmm.utils import (
get_default_timezone,
notify_error,
reload_nats,
AGENT_DEFER,
)
from tacticalrmm.constants import AGENT_DEFER
from core.utils import get_mesh_ws_url, send_command_with_mesh
from winupdate.serializers import WinUpdatePolicySerializer
from winupdate.tasks import bulk_check_for_updates_task, bulk_install_updates_task
from tacticalrmm.permissions import (
@ -156,7 +157,13 @@ class GetUpdateDeleteAgent(APIView):
# uninstall agent
def delete(self, request, agent_id):
agent = get_object_or_404(Agent, agent_id=agent_id)
asyncio.run(agent.nats_cmd({"func": "uninstall"}, wait=False))
code = "foo"
if agent.plat == "linux":
with open(settings.LINUX_AGENT_SCRIPT, "r") as f:
code = f.read()
asyncio.run(agent.nats_cmd({"func": "uninstall", "code": code}, wait=False))
name = agent.hostname
agent.delete()
reload_nats()
@ -327,12 +334,17 @@ def get_event_log(request, agent_id, logtype, days):
def send_raw_cmd(request, agent_id):
agent = get_object_or_404(Agent, agent_id=agent_id)
timeout = int(request.data["timeout"])
if request.data["shell"] == "custom" and request.data["custom_shell"]:
shell = request.data["custom_shell"]
else:
shell = request.data["shell"]
data = {
"func": "rawcmd",
"timeout": timeout,
"payload": {
"command": request.data["cmd"],
"shell": request.data["shell"],
"shell": shell,
},
}
@ -353,7 +365,7 @@ def send_raw_cmd(request, agent_id):
username=request.user.username,
agent=agent,
cmd=request.data["cmd"],
shell=request.data["shell"],
shell=shell,
debug_info={"ip": request._client_ip},
)
@ -429,7 +441,7 @@ def install_agent(request):
from knox.models import AuthToken
from accounts.models import User
from agents.utils import get_winagent_url
from agents.utils import get_agent_url
client_id = request.data["client"]
site_id = request.data["site"]
@ -439,26 +451,15 @@ def install_agent(request):
if not _has_perm_on_site(request.user, site_id):
raise PermissionDenied()
# response type is blob so we have to use
# status codes and render error message on the frontend
if arch == "64" and not os.path.exists(
os.path.join(settings.EXE_DIR, "meshagent.exe")
):
return notify_error(
"Missing 64 bit meshagent.exe. Upload it from Settings > Global Settings > MeshCentral"
)
if arch == "32" and not os.path.exists(
os.path.join(settings.EXE_DIR, "meshagent-x86.exe")
):
return notify_error(
"Missing 32 bit meshagent.exe. Upload it from Settings > Global Settings > MeshCentral"
)
inno = (
f"winagent-v{version}.exe" if arch == "64" else f"winagent-v{version}-x86.exe"
)
download_url = get_winagent_url(arch)
if request.data["installMethod"] == "linux":
plat = "linux"
else:
plat = "windows"
download_url = get_agent_url(arch, plat)
installer_user = User.objects.filter(is_installer_user=True).first()
@ -482,6 +483,33 @@ def install_agent(request):
file_name=request.data["fileName"],
)
elif request.data["installMethod"] == "linux":
# TODO
# linux agents are in beta for now, only available for sponsors for testing
# remove this after it's out of beta
try:
t: CodeSignToken = CodeSignToken.objects.first() # type: ignore
except:
return notify_error("Something went wrong")
if t is None:
return notify_error("Missing code signing token")
if not t.is_valid:
return notify_error("Code signing token is not valid")
from agents.utils import generate_linux_install
return generate_linux_install(
client=str(client_id),
site=str(site_id),
agent_type=request.data["agenttype"],
arch=arch,
token=token,
api=request.data["api"],
download_url=download_url,
)
elif request.data["installMethod"] == "manual":
cmd = [
inno,
@ -575,10 +603,15 @@ def recover(request, agent_id):
agent = get_object_or_404(Agent, agent_id=agent_id)
mode = request.data["mode"]
# attempt a realtime recovery, otherwise fall back to old recovery method
if mode == "tacagent" or mode == "mesh":
if mode == "tacagent":
cmd = "net stop tacticalrmm & taskkill /F /IM tacticalrmm.exe & net start tacticalrmm"
uri = get_mesh_ws_url()
asyncio.run(send_command_with_mesh(cmd, uri, agent.mesh_node_id, 1, 0))
return Response("Recovery will be attempted shortly")
elif mode == "mesh":
data = {"func": "recover", "payload": {"mode": mode}}
r = asyncio.run(agent.nats_cmd(data, timeout=10))
r = asyncio.run(agent.nats_cmd(data, timeout=20))
if r == "ok":
return Response("Successfully completed recovery")
@ -590,13 +623,6 @@ def recover(request, agent_id):
if mode == "command" and not request.data["cmd"]:
return notify_error("Command is required")
# if we've made it this far and realtime recovery didn't work,
# tacagent service is the fallback recovery so we obv can't use that to recover itself if it's down
if mode == "tacagent":
return notify_error(
"Requires RPC service to be functional. Please recover that first"
)
# we should only get here if all other methods fail
RecoveryAction(
agent=agent,
@ -701,27 +727,6 @@ def run_script(request, agent_id):
return Response(f"{script.name} will now be run on {agent.hostname}")
@api_view(["POST"])
def get_mesh_exe(request, arch):
filename = "meshagent.exe" if arch == "64" else "meshagent-x86.exe"
mesh_exe = os.path.join(settings.EXE_DIR, filename)
if not os.path.exists(mesh_exe):
return notify_error(f"File {filename} has not been uploaded.")
if settings.DEBUG:
with open(mesh_exe, "rb") as f:
response = HttpResponse(
f.read(), content_type="application/vnd.microsoft.portable-executable"
)
response["Content-Disposition"] = f"inline; filename={filename}"
return response
else:
response = HttpResponse()
response["Content-Disposition"] = f"attachment; filename={filename}"
response["X-Accel-Redirect"] = f"/private/exe/{filename}"
return response
class GetAddNotes(APIView):
permission_classes = [IsAuthenticated, AgentNotesPerms]
@ -819,6 +824,11 @@ def bulk(request):
elif request.data["monType"] == "workstations":
q = q.filter(monitoring_type="workstation")
if request.data["osType"] == "windows":
q = q.filter(plat="windows")
else:
q = q.filter(plat="linux")
agents: list[int] = [agent.pk for agent in q]
if not agents:
@ -832,10 +842,15 @@ def bulk(request):
)
if request.data["mode"] == "command":
if request.data["shell"] == "custom" and request.data["custom_shell"]:
shell = request.data["custom_shell"]
else:
shell = request.data["shell"]
handle_bulk_command_task.delay(
agents,
request.data["cmd"],
request.data["shell"],
shell,
request.data["timeout"],
request.user.username[:50],
run_on_offline=request.data["offlineAgents"],

View File

@ -1,9 +1,7 @@
import asyncio
import os
import time
from django.conf import settings
from django.http import HttpResponse
from django.shortcuts import get_object_or_404
from django.utils import timezone as djangotime
from packaging import version as pyver
@ -24,6 +22,9 @@ from logs.models import PendingAction, DebugLog
from software.models import InstalledSoftware
from tacticalrmm.utils import notify_error, reload_nats
from winupdate.models import WinUpdate, WinUpdatePolicy
from core.models import CoreSettings
from core.utils import get_mesh_ws_url, get_mesh_device_id, download_mesh_agent
from tacticalrmm.constants import MeshAgentIdent
class CheckIn(APIView):
@ -315,25 +316,18 @@ class MeshExe(APIView):
"""Sends the mesh exe to the installer"""
def post(self, request):
exe = "meshagent.exe" if request.data["arch"] == "64" else "meshagent-x86.exe"
mesh_exe = os.path.join(settings.EXE_DIR, exe)
match request.data:
case {"arch": "64", "plat": "windows"}:
arch = MeshAgentIdent.WIN64
case {"arch": "32", "plat": "windows"}:
arch = MeshAgentIdent.WIN32
if not os.path.exists(mesh_exe):
return notify_error("Mesh Agent executable not found")
core: CoreSettings = CoreSettings.objects.first() # type: ignore
if settings.DEBUG:
with open(mesh_exe, "rb") as f:
response = HttpResponse(
f.read(),
content_type="application/vnd.microsoft.portable-executable",
)
response["Content-Disposition"] = f"inline; filename={exe}"
return response
else:
response = HttpResponse()
response["Content-Disposition"] = f"attachment; filename={exe}"
response["X-Accel-Redirect"] = f"/private/exe/{exe}"
return response
uri = get_mesh_ws_url()
mesh_id = asyncio.run(get_mesh_device_id(uri, core.mesh_device_group))
dl_url = f"{core.mesh_site}/meshagents?id={arch}&meshid={mesh_id}&installflags=0" # type: ignore
return download_mesh_agent(dl_url)
class NewAgent(APIView):
@ -354,11 +348,11 @@ class NewAgent(APIView):
monitoring_type=request.data["monitoring_type"],
description=request.data["description"],
mesh_node_id=request.data["mesh_node_id"],
goarch=request.data["goarch"],
plat=request.data["plat"],
last_seen=djangotime.now(),
)
agent.save()
agent.salt_id = f"{agent.hostname}-{agent.pk}"
agent.save(update_fields=["salt_id"])
user = User.objects.create_user( # type: ignore
username=request.data["agent_id"],
@ -386,13 +380,8 @@ class NewAgent(APIView):
debug_info={"ip": request._client_ip},
)
return Response(
{
"pk": agent.pk,
"saltid": f"{agent.hostname}-{agent.pk}",
"token": token.key,
}
)
ret = {"pk": agent.pk, "token": token.key}
return Response(ret)
class Software(APIView):

View File

@ -135,86 +135,41 @@ class Policy(BaseAuditModel):
# List of all tasks to be applied
tasks = list()
added_task_pks = list()
agent_tasks_parent_pks = [
task.parent_task for task in agent.autotasks.filter(managed_by_policy=True)
]
# Get policies applied to agent and agent site and client
client = agent.client
site = agent.site
policies = agent.get_agent_policies()
default_policy = None
client_policy = None
site_policy = None
agent_policy = agent.policy
# Get the Client/Site policy based on if the agent is server or workstation
if agent.monitoring_type == "server":
default_policy = CoreSettings.objects.first().server_policy
client_policy = client.server_policy
site_policy = site.server_policy
elif agent.monitoring_type == "workstation":
default_policy = CoreSettings.objects.first().workstation_policy
client_policy = client.workstation_policy
site_policy = site.workstation_policy
# check if client/site/agent is blocking inheritance and blank out policies
if agent.block_policy_inheritance:
site_policy = None
client_policy = None
default_policy = None
elif site.block_policy_inheritance:
client_policy = None
default_policy = None
elif client.block_policy_inheritance:
default_policy = None
if (
agent_policy
and agent_policy.active
and not agent_policy.is_agent_excluded(agent)
):
for task in agent_policy.autotasks.all():
if task.pk not in added_task_pks:
if policies["agent_policy"] and policies["agent_policy"].active:
for task in policies["agent_policy"].autotasks.all():
if task.pk not in [task.pk for task in tasks]:
tasks.append(task)
added_task_pks.append(task.pk)
if (
site_policy
and site_policy.active
and not site_policy.is_agent_excluded(agent)
):
for task in site_policy.autotasks.all():
if task.pk not in added_task_pks:
if policies["site_policy"] and policies["site_policy"].active:
for task in policies["site_policy"].autotasks.all():
if task.pk not in [task.pk for task in tasks]:
tasks.append(task)
added_task_pks.append(task.pk)
if (
client_policy
and client_policy.active
and not client_policy.is_agent_excluded(agent)
):
for task in client_policy.autotasks.all():
if task.pk not in added_task_pks:
if policies["client_policy"] and policies["client_policy"].active:
for task in policies["client_policy"].autotasks.all():
if task.pk not in [task.pk for task in tasks]:
tasks.append(task)
added_task_pks.append(task.pk)
if (
default_policy
and default_policy.active
and not default_policy.is_agent_excluded(agent)
):
for task in default_policy.autotasks.all():
if task.pk not in added_task_pks:
if policies["default_policy"] and policies["default_policy"].active:
for task in policies["default_policy"].autotasks.all():
if task.pk not in [task.pk for task in tasks]:
tasks.append(task)
added_task_pks.append(task.pk)
# remove policy tasks that use scripts that aren't compatible with the agent platform
tasks = [task for task in tasks if agent.is_supported_script(task.script.shell)]
# remove policy tasks from agent not included in policy
for task in agent.autotasks.filter(
parent_task__in=[
taskpk
for taskpk in agent_tasks_parent_pks
if taskpk not in added_task_pks
if taskpk not in [task.pk for task in tasks]
]
):
if task.sync_status == "initial":
@ -225,7 +180,7 @@ class Policy(BaseAuditModel):
# change tasks from pendingdeletion to notsynced if policy was added or changed
agent.autotasks.filter(sync_status="pendingdeletion").filter(
parent_task__in=[taskpk for taskpk in added_task_pks]
parent_task__in=[taskpk for taskpk in [task.pk for task in tasks]]
).update(sync_status="notsynced")
return [task for task in tasks if task.pk not in agent_tasks_parent_pks]
@ -241,85 +196,43 @@ class Policy(BaseAuditModel):
]
# Get policies applied to agent and agent site and client
client = agent.client
site = agent.site
default_policy = None
client_policy = None
site_policy = None
agent_policy = agent.policy
if agent.monitoring_type == "server":
default_policy = CoreSettings.objects.first().server_policy
client_policy = client.server_policy
site_policy = site.server_policy
elif agent.monitoring_type == "workstation":
default_policy = CoreSettings.objects.first().workstation_policy
client_policy = client.workstation_policy
site_policy = site.workstation_policy
# check if client/site/agent is blocking inheritance and blank out policies
if agent.block_policy_inheritance:
site_policy = None
client_policy = None
default_policy = None
elif site.block_policy_inheritance:
client_policy = None
default_policy = None
elif client.block_policy_inheritance:
default_policy = None
policies = agent.get_agent_policies()
# Used to hold the policies that will be applied and the order in which they are applied
# Enforced policies are applied first
enforced_checks = list()
policy_checks = list()
if (
agent_policy
and agent_policy.active
and not agent_policy.is_agent_excluded(agent)
):
if agent_policy.enforced:
for check in agent_policy.policychecks.all():
if policies["agent_policy"] and policies["agent_policy"].active:
if policies["agent_policy"].enforced:
for check in policies["agent_policy"].policychecks.all():
enforced_checks.append(check)
else:
for check in agent_policy.policychecks.all():
for check in policies["agent_policy"].policychecks.all():
policy_checks.append(check)
if (
site_policy
and site_policy.active
and not site_policy.is_agent_excluded(agent)
):
if site_policy.enforced:
for check in site_policy.policychecks.all():
if policies["site_policy"] and policies["site_policy"].active:
if policies["site_policy"].enforced:
for check in policies["site_policy"].policychecks.all():
enforced_checks.append(check)
else:
for check in site_policy.policychecks.all():
for check in policies["site_policy"].policychecks.all():
policy_checks.append(check)
if (
client_policy
and client_policy.active
and not client_policy.is_agent_excluded(agent)
):
if client_policy.enforced:
for check in client_policy.policychecks.all():
if policies["client_policy"] and policies["client_policy"].active:
if policies["client_policy"].enforced:
for check in policies["client_policy"].policychecks.all():
enforced_checks.append(check)
else:
for check in client_policy.policychecks.all():
for check in policies["client_policy"].policychecks.all():
policy_checks.append(check)
if (
default_policy
and default_policy.active
and not default_policy.is_agent_excluded(agent)
):
if default_policy.enforced:
for check in default_policy.policychecks.all():
if policies["default_policy"] and policies["default_policy"].active:
if policies["default_policy"].enforced:
for check in policies["default_policy"].policychecks.all():
enforced_checks.append(check)
else:
for check in default_policy.policychecks.all():
for check in policies["default_policy"].policychecks.all():
policy_checks.append(check)
# Sorted Checks already added
@ -342,7 +255,7 @@ class Policy(BaseAuditModel):
# Loop over checks in with enforced policies first, then non-enforced policies
for check in enforced_checks + agent_checks + policy_checks:
if check.check_type == "diskspace":
if check.check_type == "diskspace" and agent.plat == "windows":
# Check if drive letter was already added
if check.disk not in added_diskspace_checks:
added_diskspace_checks.append(check.disk)
@ -364,7 +277,7 @@ class Policy(BaseAuditModel):
check.overriden_by_policy = True
check.save()
if check.check_type == "cpuload":
if check.check_type == "cpuload" and agent.plat == "windows":
# Check if cpuload list is empty
if not added_cpuload_checks:
added_cpuload_checks.append(check)
@ -375,7 +288,7 @@ class Policy(BaseAuditModel):
check.overriden_by_policy = True
check.save()
if check.check_type == "memory":
if check.check_type == "memory" and agent.plat == "windows":
# Check if memory check list is empty
if not added_memory_checks:
added_memory_checks.append(check)
@ -386,7 +299,7 @@ class Policy(BaseAuditModel):
check.overriden_by_policy = True
check.save()
if check.check_type == "winsvc":
if check.check_type == "winsvc" and agent.plat == "windows":
# Check if service name was already added
if check.svc_name not in added_winsvc_checks:
added_winsvc_checks.append(check.svc_name)
@ -397,7 +310,9 @@ class Policy(BaseAuditModel):
check.overriden_by_policy = True
check.save()
if check.check_type == "script":
if check.check_type == "script" and agent.is_supported_script(
check.script.shell
):
# Check if script id was already added
if check.script.id not in added_script_checks:
added_script_checks.append(check.script.id)
@ -408,7 +323,7 @@ class Policy(BaseAuditModel):
check.overriden_by_policy = True
check.save()
if check.check_type == "eventlog":
if check.check_type == "eventlog" and agent.plat == "windows":
# Check if events were already added
if [check.log_name, check.event_id] not in added_eventlog_checks:
added_eventlog_checks.append([check.log_name, check.event_id])

View File

@ -69,7 +69,7 @@ class TestPolicyViews(TacticalTestCase):
# create policy with tasks and checks
policy = baker.make("automation.Policy")
checks = self.create_checks(policy=policy)
tasks = baker.make("autotasks.AutomatedTask", policy=policy, _quantity=3)
tasks = baker.make_recipe("autotasks.task", policy=policy, _quantity=3)
# assign a task to a check
tasks[0].assigned_check = checks[0] # type: ignore
@ -248,11 +248,11 @@ class TestPolicyViews(TacticalTestCase):
# policy with a task
policy = baker.make("automation.Policy")
task = baker.make("autotasks.AutomatedTask", policy=policy)
task = baker.make_recipe("autotasks.task", policy=policy)
# create policy managed tasks
policy_tasks = baker.make(
"autotasks.AutomatedTask", parent_task=task.id, _quantity=5 # type: ignore
policy_tasks = baker.make_recipe(
"autotasks.task", parent_task=task.id, _quantity=5 # type: ignore
)
url = f"/automation/tasks/{task.id}/status/" # type: ignore
@ -269,8 +269,8 @@ class TestPolicyViews(TacticalTestCase):
def test_run_win_task(self, mock_task):
# create managed policy tasks
tasks = baker.make(
"autotasks.AutomatedTask",
tasks = baker.make_recipe(
"autotasks.task",
managed_by_policy=True,
parent_task=1,
_quantity=6,
@ -577,8 +577,8 @@ class TestPolicyTasks(TacticalTestCase):
policy = baker.make("automation.Policy", active=True)
self.create_checks(policy=policy)
baker.make(
"autotasks.AutomatedTask", policy=policy, name=seq("Task"), _quantity=3
baker.make_recipe(
"autotasks.task", policy=policy, name=seq("Task"), _quantity=3
)
server_agent = baker.make_recipe("agents.server_agent")
@ -859,8 +859,8 @@ class TestPolicyTasks(TacticalTestCase):
# create test data
policy = baker.make("automation.Policy", active=True)
tasks = baker.make(
"autotasks.AutomatedTask", policy=policy, name=seq("Task"), _quantity=3
tasks = baker.make_recipe(
"autotasks.task", policy=policy, name=seq("Task"), _quantity=3
)
agent = baker.make_recipe("agents.server_agent", policy=policy)
@ -889,7 +889,7 @@ class TestPolicyTasks(TacticalTestCase):
from .tasks import delete_policy_autotasks_task, generate_agent_checks_task
policy = baker.make("automation.Policy", active=True)
tasks = baker.make("autotasks.AutomatedTask", policy=policy, _quantity=3)
tasks = baker.make_recipe("autotasks.task", policy=policy, _quantity=3)
agent = baker.make_recipe("agents.server_agent", policy=policy)
generate_agent_checks_task(agents=[agent.pk], create_tasks=True)
@ -904,7 +904,7 @@ class TestPolicyTasks(TacticalTestCase):
from .tasks import run_win_policy_autotasks_task, generate_agent_checks_task
policy = baker.make("automation.Policy", active=True)
tasks = baker.make("autotasks.AutomatedTask", policy=policy, _quantity=3)
tasks = baker.make_recipe("autotasks.task", policy=policy, _quantity=3)
agent = baker.make_recipe("agents.server_agent", policy=policy)
generate_agent_checks_task(agents=[agent.pk], create_tasks=True)
@ -923,8 +923,8 @@ class TestPolicyTasks(TacticalTestCase):
# setup data
policy = baker.make("automation.Policy", active=True)
tasks = baker.make(
"autotasks.AutomatedTask",
tasks = baker.make_recipe(
"autotasks.task",
enabled=True,
policy=policy,
_quantity=3,
@ -977,7 +977,7 @@ class TestPolicyTasks(TacticalTestCase):
# setup data
policy = baker.make("automation.Policy", active=True)
baker.make_recipe("checks.memory_check", policy=policy)
task = baker.make("autotasks.AutomatedTask", policy=policy)
task = baker.make_recipe("autotasks.task", policy=policy)
agent = baker.make_recipe(
"agents.agent", policy=policy, monitoring_type="server"
)
@ -1072,7 +1072,7 @@ class TestPolicyTasks(TacticalTestCase):
# setup data
policy = baker.make("automation.Policy", active=True)
baker.make_recipe("checks.memory_check", policy=policy)
baker.make("autotasks.AutomatedTask", policy=policy)
baker.make_recipe("autotasks.task", policy=policy)
agent = baker.make_recipe("agents.agent", monitoring_type="server")
core = CoreSettings.objects.first()

View File

@ -0,0 +1,8 @@
from itertools import cycle
from model_bakery.recipe import Recipe, seq, foreign_key
script = Recipe("scripts.script")
task = Recipe(
"autotasks.AutomatedTask",
script=foreign_key(script),
)

View File

@ -6,7 +6,7 @@ from django.db import models
from agents.models import Agent
from logs.models import BaseAuditModel
from tacticalrmm.models import PermissionQuerySet
from tacticalrmm.utils import AGENT_DEFER
from tacticalrmm.constants import AGENT_DEFER
def _default_failing_checks_data():

View File

@ -0,0 +1,131 @@
#!/usr/bin/env bash
if [ $EUID -ne 0 ]; then
echo "ERROR: Must be run as root"
exit 1
fi
####
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}"
meshDir='/opt/tacmesh'
meshSystemBin="${meshDir}/meshagent"
RemoveOldAgent() {
if [ -f "${agentSysD}" ]; then
systemctl disable --now tacticalagent.service
rm -f ${agentSysD}
systemctl daemon-reload
fi
if [ -f "${agentConf}" ]; then
rm -f ${agentConf}
fi
if [ -f "${agentBin}" ]; then
rm -f ${agentBin}
fi
}
InstallMesh() {
meshTmpDir=$(mktemp -d -t "mesh-XXXXXXXXX")
meshTmpBin="${meshTmpDir}/meshagent"
wget -q -O ${meshTmpBin} ${meshDL}
chmod +x ${meshTmpBin}
mkdir -p ${meshDir}
${meshTmpBin} -install --installPath=${meshDir}
sleep 1
rm -rf ${meshTmpDir}
}
RemoveMesh() {
${meshSystemBin} -uninstall
sleep 1
rm -rf ${meshDir}
systemctl daemon-reload
}
Uninstall() {
RemoveMesh
RemoveOldAgent
}
if [ $# -ne 0 ] && [ $1 == 'uninstall' ]; then
Uninstall
exit 0
fi
RemoveOldAgent
echo "Downloading tactical agent..."
wget -q -O ${agentBin} "${agentDL}"
chmod +x ${agentBin}
MESH_NODE_ID=""
if [ $# -ne 0 ] && [ $1 == '--nomesh' ]; 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=$(${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 [ "${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 --now ${agentSvcName}

View File

@ -1,26 +1,16 @@
import asyncio
import json
import websockets
from django.conf import settings
from django.core.management.base import BaseCommand
from core.models import CoreSettings
from .helpers import get_auth_token
from core.utils import get_mesh_ws_url
class Command(BaseCommand):
help = "Sets up initial mesh central configuration"
async def websocket_call(self, mesh_settings):
token = get_auth_token(mesh_settings.mesh_username, mesh_settings.mesh_token)
if settings.DOCKER_BUILD:
uri = f"{settings.MESH_WS_URL}/control.ashx?auth={token}"
else:
site = mesh_settings.mesh_site.replace("https", "wss")
uri = f"{site}/control.ashx?auth={token}"
async def websocket_call(self, uri):
async with websockets.connect(uri) as websocket:
@ -41,9 +31,9 @@ class Command(BaseCommand):
response = json.loads(message)
if response["action"] == "createInviteLink":
print(response["url"].replace(":4443", ":443"))
self.stdout.write(response["url"].replace(":4443", ":443"))
break
def handle(self, *args, **kwargs):
mesh_settings = CoreSettings.objects.first()
asyncio.get_event_loop().run_until_complete(self.websocket_call(mesh_settings))
uri = get_mesh_ws_url()
asyncio.run(self.websocket_call(uri))

View File

@ -1,19 +0,0 @@
import time
from base64 import b64encode
from Crypto.Cipher import AES
from Crypto.Random import get_random_bytes
def get_auth_token(user, key):
key = bytes.fromhex(key)
key1 = key[0:32]
msg = '{{"userid":"{}", "domainid":"{}", "time":{}}}'.format(
f"user//{user}", "", int(time.time())
)
iv = get_random_bytes(12)
a = AES.new(key1, AES.MODE_GCM, iv)
msg, tag = a.encrypt_and_digest(bytes(msg, "utf-8"))
return b64encode(iv + tag + msg, altchars=b"@$").decode("utf-8")

View File

@ -6,22 +6,13 @@ from django.conf import settings
from django.core.management.base import BaseCommand
from core.models import CoreSettings
from .helpers import get_auth_token
from core.utils import get_mesh_ws_url
class Command(BaseCommand):
help = "Sets up initial mesh central configuration"
async def websocket_call(self, mesh_settings):
token = get_auth_token(mesh_settings.mesh_username, mesh_settings.mesh_token)
if settings.DOCKER_BUILD:
uri = f"{settings.MESH_WS_URL}/control.ashx?auth={token}"
else:
site = mesh_settings.mesh_site.replace("https", "wss")
uri = f"{site}/control.ashx?auth={token}"
async def websocket_call(self, uri):
async with websockets.connect(uri) as websocket:
@ -82,9 +73,8 @@ class Command(BaseCommand):
return
try:
asyncio.get_event_loop().run_until_complete(
self.websocket_call(mesh_settings)
)
uri = get_mesh_ws_url()
asyncio.run(self.websocket_call(uri))
self.stdout.write("Initial Mesh Central setup complete")
except websockets.exceptions.ConnectionClosedError:
self.stdout.write(

View File

@ -3,10 +3,11 @@ from django.core.management.base import BaseCommand
from django.utils.timezone import make_aware
import datetime as dt
from logs.models import PendingAction
from scripts.models import Script
from autotasks.models import AutomatedTask
from accounts.models import User
from agents.models import Agent
from tacticalrmm.constants import AGENT_DEFER
class Command(BaseCommand):
@ -15,9 +16,6 @@ class Command(BaseCommand):
def handle(self, *args, **kwargs):
self.stdout.write("Running post update tasks")
# remove task pending actions. deprecated 4/20/2021
PendingAction.objects.filter(action_type="taskaction").delete()
# load community scripts into the db
Script.load_community_scripts()
@ -71,4 +69,16 @@ class Command(BaseCommand):
except:
continue
# set goarch for older windows agents
for agent in Agent.objects.defer(*AGENT_DEFER):
if not agent.goarch:
if agent.arch == "64":
agent.goarch = "amd64"
elif agent.arch == "32":
agent.goarch = "386"
else:
agent.goarch = "amd64"
agent.save(update_fields=["goarch"])
self.stdout.write("Post update tasks finished")

View File

@ -0,0 +1,18 @@
# Generated by Django 3.2.12 on 2022-02-16 21:18
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0029_alter_coresettings_default_time_zone'),
]
operations = [
migrations.AddField(
model_name='coresettings',
name='mesh_device_group',
field=models.CharField(blank=True, default='TacticalRMM', max_length=255, null=True),
),
]

View File

@ -61,6 +61,9 @@ class CoreSettings(BaseAuditModel):
mesh_token = models.CharField(max_length=255, null=True, blank=True, default="")
mesh_username = models.CharField(max_length=255, null=True, blank=True, default="")
mesh_site = models.CharField(max_length=255, null=True, blank=True, default="")
mesh_device_group = models.CharField(
max_length=255, null=True, blank=True, default="TacticalRMM"
)
agent_auto_update = models.BooleanField(default=True)
workstation_policy = models.ForeignKey(
"automation.Policy",
@ -319,22 +322,14 @@ class CodeSignToken(models.Model):
if not self.token:
return False
errors = []
for url in settings.EXE_GEN_URLS:
try:
r = requests.post(
f"{url}/api/v1/checktoken",
json={"token": self.token},
headers={"Content-type": "application/json"},
timeout=15,
)
except Exception as e:
errors.append(str(e))
else:
errors = []
break
if errors:
try:
r = requests.post(
f"{settings.EXE_GEN_URL}/api/v1/checktoken",
json={"token": self.token},
headers={"Content-type": "application/json"},
timeout=15,
)
except:
return False
return r.status_code == 200

View File

@ -11,7 +11,7 @@ from alerts.tasks import prune_resolved_alerts
from core.models import CoreSettings
from logs.tasks import prune_debug_log, prune_audit_log
from tacticalrmm.celery import app
from tacticalrmm.utils import AGENT_DEFER
from tacticalrmm.constants import AGENT_DEFER
from agents.models import Agent
from clients.models import Client, Site
from alerts.models import Alert

View File

@ -3,7 +3,6 @@ from django.urls import path
from . import views
urlpatterns = [
path("uploadmesh/", views.UploadMeshAgent.as_view()),
path("settings/", views.GetEditCoreSettings.as_view()),
path("version/", views.version),
path("emailtest/", views.email_test),

View File

@ -0,0 +1,88 @@
import time
import json
import requests
import tempfile
import websockets
from base64 import b64encode
from Crypto.Cipher import AES
from Crypto.Random import get_random_bytes
from django.http import FileResponse
from django.conf import settings
def get_auth_token(user, key):
key = bytes.fromhex(key)
key1 = key[0:32]
msg = '{{"userid":"{}", "domainid":"{}", "time":{}}}'.format(
f"user//{user}", "", int(time.time())
)
iv = get_random_bytes(12)
a = AES.new(key1, AES.MODE_GCM, iv)
msg, tag = a.encrypt_and_digest(bytes(msg, "utf-8")) # type: ignore
return b64encode(iv + tag + msg, altchars=b"@$").decode("utf-8")
def get_mesh_ws_url() -> str:
from core.models import CoreSettings
core = CoreSettings.objects.first()
token = get_auth_token(core.mesh_username, core.mesh_token) # type: ignore
if settings.DOCKER_BUILD:
uri = f"{settings.MESH_WS_URL}/control.ashx?auth={token}"
else:
site = core.mesh_site.replace("https", "wss") # type: ignore
uri = f"{site}/control.ashx?auth={token}"
return uri
async def get_mesh_device_id(uri: str, device_group: str):
async with websockets.connect(uri) as ws: # type: ignore
payload = {"action": "meshes", "responseid": "meshctrl"}
await ws.send(json.dumps(payload))
async for message in ws:
r = json.loads(message)
if r["action"] == "meshes":
return list(filter(lambda x: x["name"] == device_group, r["meshes"]))[
0
]["_id"].split("mesh//")[1]
def download_mesh_agent(dl_url: str) -> FileResponse:
with tempfile.NamedTemporaryFile(prefix="mesh-", dir=settings.EXE_DIR) as fp:
r = requests.get(dl_url, stream=True, timeout=15)
with open(fp.name, "wb") as f:
for chunk in r.iter_content(chunk_size=1024):
if chunk:
f.write(chunk)
del r
return FileResponse(open(fp.name, "rb"), as_attachment=True, filename=fp.name)
def _b64_to_hex(h):
return b64encode(bytes.fromhex(h)).decode().replace(r"/", "$").replace(r"+", "@")
async def send_command_with_mesh(
cmd: str, uri: str, mesh_node_id: str, shell: int, run_as_user: int
):
node_id = _b64_to_hex(mesh_node_id)
async with websockets.connect(uri) as ws: # type: ignore
await ws.send(
json.dumps(
{
"action": "runcommands",
"cmds": cmd,
"nodeids": [f"node//{node_id}"],
"runAsUser": run_as_user,
"type": shell,
"responseid": "trmm",
}
)
)

View File

@ -36,28 +36,6 @@ from .serializers import (
)
class UploadMeshAgent(APIView):
permission_classes = [IsAuthenticated, CoreSettingsPerms]
parser_class = (FileUploadParser,)
def put(self, request, format=None):
if "meshagent" not in request.data and "arch" not in request.data:
raise ParseError("Empty content")
arch = request.data["arch"]
f = request.data["meshagent"]
mesh_exe = os.path.join(
settings.EXE_DIR, "meshagent.exe" if arch == "64" else "meshagent-x86.exe"
)
with open(mesh_exe, "wb+") as j:
for chunk in f.chunks():
j.write(chunk)
return Response(
"Mesh Agent uploaded successfully", status=status.HTTP_201_CREATED
)
class GetEditCoreSettings(APIView):
permission_classes = [IsAuthenticated, CoreSettingsPerms]
@ -232,23 +210,15 @@ class CodeSign(APIView):
def patch(self, request):
import requests
errors = []
for url in settings.EXE_GEN_URLS:
try:
r = requests.post(
f"{url}/api/v1/checktoken",
json={"token": request.data["token"]},
headers={"Content-type": "application/json"},
timeout=15,
)
except Exception as e:
errors.append(str(e))
else:
errors = []
break
if errors:
return notify_error(", ".join(errors))
try:
r = requests.post(
f"{settings.EXE_GEN_URL}/api/v1/checktoken",
json={"token": request.data["token"]},
headers={"Content-type": "application/json"},
timeout=15,
)
except Exception as e:
return notify_error(str(e))
if r.status_code == 400 or r.status_code == 401: # type: ignore
return notify_error(r.json()["ret"]) # type: ignore

View File

@ -0,0 +1,18 @@
# Generated by Django 3.2.12 on 2022-02-27 05:54
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('logs', '0022_auto_20211105_0158'),
]
operations = [
migrations.AlterField(
model_name='pendingaction',
name='action_type',
field=models.CharField(blank=True, choices=[('schedreboot', 'Scheduled Reboot'), ('agentupdate', 'Agent Update'), ('chocoinstall', 'Chocolatey Software Install'), ('runcmd', 'Run Command'), ('runscript', 'Run Script'), ('runpatchscan', 'Run Patch Scan'), ('runpatchinstall', 'Run Patch Install')], max_length=255, null=True),
),
]

View File

@ -14,7 +14,6 @@ def get_debug_level():
ACTION_TYPE_CHOICES = [
("schedreboot", "Scheduled Reboot"),
("taskaction", "Scheduled Task Action"), # deprecated
("agentupdate", "Agent Update"),
("chocoinstall", "Chocolatey Software Install"),
("runcmd", "Run Command"),

View File

@ -19,7 +19,6 @@ class AuditLogSerializer(serializers.ModelSerializer):
class PendingActionSerializer(serializers.ModelSerializer):
hostname = serializers.ReadOnlyField(source="agent.hostname")
salt_id = serializers.ReadOnlyField(source="agent.salt_id")
client = serializers.ReadOnlyField(source="agent.client.name")
site = serializers.ReadOnlyField(source="agent.site.name")
due = serializers.ReadOnlyField()

View File

@ -9,7 +9,8 @@ from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.views import APIView
from rest_framework.exceptions import PermissionDenied
from tacticalrmm.utils import notify_error, get_default_timezone, AGENT_DEFER
from tacticalrmm.utils import notify_error, get_default_timezone
from tacticalrmm.constants import AGENT_DEFER
from tacticalrmm.permissions import _audit_log_filter, _has_perm_on_agent
from .models import AuditLog, PendingAction, DebugLog

View File

@ -23,15 +23,15 @@ pyotp==2.6.0
pyparsing==3.0.7
pytz==2021.3
qrcode==7.3.1
redis==4.1.3
redis==4.1.4
requests==2.27.1
six==1.16.0
sqlparse==0.4.2
twilio==7.6.0
twilio==7.7.0
urllib3==1.26.8
uWSGI==2.0.20
validators==0.18.2
vine==5.0.0
websockets==10.1
websockets==10.2
zipp==3.7.0
drf_spectacular==0.21.2

View File

@ -0,0 +1,23 @@
# Generated by Django 3.2.12 on 2022-02-17 14:46
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('scripts', '0015_auto_20211128_1637'),
]
operations = [
migrations.AlterField(
model_name='script',
name='shell',
field=models.CharField(choices=[('powershell', 'Powershell'), ('cmd', 'Batch (CMD)'), ('python', 'Python'), ('shell', 'Shell')], default='powershell', max_length=100),
),
migrations.AlterField(
model_name='scriptsnippet',
name='shell',
field=models.CharField(choices=[('powershell', 'Powershell'), ('cmd', 'Batch (CMD)'), ('python', 'Python'), ('shell', 'Shell')], default='powershell', max_length=15),
),
]

View File

@ -13,6 +13,7 @@ SCRIPT_SHELLS = [
("powershell", "Powershell"),
("cmd", "Batch (CMD)"),
("python", "Python"),
("shell", "Shell")
]
SCRIPT_TYPES = [

View File

@ -0,0 +1,53 @@
from enum import Enum
class MeshAgentIdent(Enum):
WIN32 = 3
WIN64 = 4
LINUX32 = 5
LINUX64 = 6
LINUX_ARM_64 = 26
LINUX_ARM_HF = 25
def __str__(self):
return str(self.value)
AGENT_DEFER = ("wmi_detail", "services")
WEEK_DAYS = {
"Sunday": 0x1,
"Monday": 0x2,
"Tuesday": 0x4,
"Wednesday": 0x8,
"Thursday": 0x10,
"Friday": 0x20,
"Saturday": 0x40,
}
MONTHS = {
"January": 0x1,
"February": 0x2,
"March": 0x4,
"April": 0x8,
"May": 0x10,
"June": 0x20,
"July": 0x40,
"August": 0x80,
"September": 0x100,
"October": 0x200,
"November": 0x400,
"December": 0x800,
}
WEEKS = {
"First Week": 0x1,
"Second Week": 0x2,
"Third Week": 0x4,
"Fourth Week": 0x8,
"Last Week": 0x10,
}
MONTH_DAYS = {f"{b}": 0x1 << a for a, b in enumerate(range(1, 32))}
MONTH_DAYS["Last Day"] = 0x80000000

View File

@ -105,9 +105,7 @@ class DemoMiddleware:
{"name": "update_agents", "methods": ["POST"]},
{"name": "send_raw_cmd", "methods": ["POST"]},
{"name": "install_agent", "methods": ["POST"]},
{"name": "get_mesh_exe", "methods": ["POST"]},
{"name": "GenerateAgent", "methods": ["GET"]},
{"name": "UploadMeshAgent", "methods": ["PUT"]},
{"name": "email_test", "methods": ["POST"]},
{"name": "server_maintenance", "methods": ["POST"]},
{"name": "CodeSign", "methods": ["PATCH", "POST"]},
@ -151,3 +149,42 @@ class DemoMiddleware:
for i in self.not_allowed:
if view_func.__name__ == i["name"] and request.method in i["methods"]:
return self.drf_mock_response(request, notify_error(err))
class LinuxMiddleware:
def __init__(self, get_response):
self.get_response = get_response
self.not_implemented = [
{"name": "ScanWindowsUpdates", "methods": ["POST"]},
{"name": "GetSoftware", "methods": ["POST", "PUT"]},
]
def __call__(self, request):
return self.get_response(request)
def drf_mock_response(self, request, resp):
from rest_framework.views import APIView
view = APIView()
view.headers = view.default_response_headers
return view.finalize_response(request, resp).render() # type: ignore
def process_view(self, request, view_func, view_args, view_kwargs):
if not request.path.startswith(EXCLUDE_PATHS):
if "agent_id" in view_kwargs.keys():
from agents.models import Agent
err = "Not currently implemented for linux"
agent = Agent.objects.only("id", "agent_id", "plat").get(
agent_id=view_kwargs["agent_id"]
)
if agent.plat == "linux":
from .utils import notify_error
for i in self.not_implemented:
if (
view_func.__name__ == i["name"]
and request.method in i["methods"]
):
return self.drf_mock_response(request, notify_error(err))

View File

@ -12,21 +12,23 @@ LOG_DIR = os.path.join(BASE_DIR, "tacticalrmm/private/log")
EXE_DIR = os.path.join(BASE_DIR, "tacticalrmm/private/exe")
LINUX_AGENT_SCRIPT = BASE_DIR / "core" / "agent_linux.sh"
AUTH_USER_MODEL = "accounts.User"
# latest release
TRMM_VERSION = "0.11.3"
TRMM_VERSION = "0.12.0"
# bump this version everytime vue code is changed
# to alert user they need to manually refresh their browser
APP_VER = "0.0.157"
# https://github.com/wh1te909/rmmagent
LATEST_AGENT_VER = "1.8.0"
LATEST_AGENT_VER = "2.0.1"
MESH_VER = "0.9.79"
MESH_VER = "0.9.95"
NATS_SERVER_VER = "2.7.2"
NATS_SERVER_VER = "2.7.3"
# for the update script, bump when need to recreate venv or npm install
PIP_VER = "26"
@ -38,10 +40,7 @@ WHEEL_VER = "0.37.1"
DL_64 = f"https://github.com/wh1te909/rmmagent/releases/download/v{LATEST_AGENT_VER}/winagent-v{LATEST_AGENT_VER}.exe"
DL_32 = f"https://github.com/wh1te909/rmmagent/releases/download/v{LATEST_AGENT_VER}/winagent-v{LATEST_AGENT_VER}-x86.exe"
EXE_GEN_URLS = [
"https://exe2.tacticalrmm.io",
"https://exe.tacticalrmm.io",
]
EXE_GEN_URL = "https://agents.tacticalrmm.com"
DEFAULT_AUTO_FIELD = "django.db.models.AutoField"
@ -139,6 +138,7 @@ MIDDLEWARE = [
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"tacticalrmm.middleware.AuditMiddleware",
"tacticalrmm.middleware.LinuxMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
]
@ -207,11 +207,18 @@ STATICFILES_DIRS = [os.path.join(BASE_DIR, "tacticalrmm/static/")]
LOGGING = {
"version": 1,
"disable_existing_loggers": False,
"formatters": {
"verbose": {
"format": "[%(asctime)s] %(levelname)s [%(name)s:%(lineno)s] %(message)s",
"datefmt": "%d/%b/%Y %H:%M:%S",
},
},
"handlers": {
"file": {
"level": "ERROR",
"class": "logging.FileHandler",
"filename": os.path.join(LOG_DIR, "django_debug.log"),
"formatter": "verbose",
}
},
"loggers": {

View File

@ -9,8 +9,8 @@ from .utils import (
generate_winagent_exe,
get_bit_days,
reload_nats,
AGENT_DEFER,
)
from tacticalrmm.constants import AGENT_DEFER
class TestUtils(TacticalTestCase):

View File

@ -19,47 +19,10 @@ from rest_framework.response import Response
from core.models import CodeSignToken
from logs.models import DebugLog
from agents.models import Agent
from tacticalrmm.constants import WEEK_DAYS, MONTHS, WEEKS, MONTH_DAYS
notify_error = lambda msg: Response(msg, status=status.HTTP_400_BAD_REQUEST)
AGENT_DEFER = ["wmi_detail", "services"]
WEEK_DAYS = {
"Sunday": 0x1,
"Monday": 0x2,
"Tuesday": 0x4,
"Wednesday": 0x8,
"Thursday": 0x10,
"Friday": 0x20,
"Saturday": 0x40,
}
MONTHS = {
"January": 0x1,
"February": 0x2,
"March": 0x4,
"April": 0x8,
"May": 0x10,
"June": 0x20,
"July": 0x40,
"August": 0x80,
"September": 0x100,
"October": 0x200,
"November": 0x400,
"December": 0x800,
}
WEEKS = {
"First Week": 0x1,
"Second Week": 0x2,
"Third Week": 0x4,
"Fourth Week": 0x8,
"Last Week": 0x10,
}
MONTH_DAYS = {f"{b}": 0x1 << a for a, b in enumerate(range(1, 32))}
MONTH_DAYS["Last Day"] = 0x80000000
def generate_winagent_exe(
client: int,
@ -74,7 +37,7 @@ def generate_winagent_exe(
file_name: str,
) -> Union[Response, FileResponse]:
from agents.utils import get_winagent_url
from agents.utils import get_agent_url
inno = (
f"winagent-v{settings.LATEST_AGENT_VER}.exe"
@ -82,7 +45,7 @@ def generate_winagent_exe(
else f"winagent-v{settings.LATEST_AGENT_VER}-x86.exe"
)
dl_url = get_winagent_url(arch)
dl_url = get_agent_url(arch, "windows")
try:
codetoken = CodeSignToken.objects.first().token # type:ignore
@ -105,25 +68,18 @@ def generate_winagent_exe(
}
headers = {"Content-type": "application/json"}
errors = []
with tempfile.NamedTemporaryFile() as fp:
for url in settings.EXE_GEN_URLS:
try:
r = requests.post(
f"{url}/api/v1/exe",
json=data,
headers=headers,
stream=True,
timeout=900,
)
except Exception as e:
errors.append(str(e))
else:
errors = []
break
if errors:
DebugLog.error(message=errors)
try:
r = requests.post(
f"{settings.EXE_GEN_URL}/api/v1/exe",
json=data,
headers=headers,
stream=True,
timeout=900,
)
except Exception as e:
DebugLog.error(message=str(e))
return notify_error(
"Something went wrong. Check debug error log for exact error message"
)

View File

@ -0,0 +1,21 @@
# Generated by Django 3.2.12 on 2022-02-27 05:54
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('winupdate', '0011_auto_20210917_1954'),
]
operations = [
migrations.RemoveField(
model_name='winupdate',
name='mandatory',
),
migrations.RemoveField(
model_name='winupdate',
name='needs_reboot',
),
]

View File

@ -44,9 +44,7 @@ class WinUpdate(models.Model):
)
guid = models.CharField(max_length=255, null=True, blank=True)
kb = models.CharField(max_length=100, null=True, blank=True)
mandatory = models.BooleanField(default=False) # deprecated
title = models.TextField(null=True, blank=True)
needs_reboot = models.BooleanField(default=False) # deprecated
installed = models.BooleanField(default=False)
downloaded = models.BooleanField(default=False)
description = models.TextField(null=True, blank=True)

View File

@ -1,21 +1,74 @@
MIT License
### Tactical RMM License Version 1.0
Copyright (c) 2019-present wh1te909
Text of license:&emsp;&emsp;&emsp;Copyright © 2022 AmidaWare LLC. All rights reserved.<br>
&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;&nbsp;Amending the text of this license is not permitted.
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:
Trade Mark:&emsp;&emsp;&emsp;&emsp;"Tactical RMM" is a trade mark of AmidaWare LLC.
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
Licensor:&emsp;&emsp;&emsp;&emsp;&emsp;&nbsp;&nbsp;AmidaWare LLC of 1968 S Coast Hwy PMB 3847 Laguna Beach, CA, USA.
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.
Licensed Software:&emsp;&nbsp;The software known as Tactical RMM Version v0.12.0 (and all subsequent releases and versions) and the Tactical RMM Agent v2.0.0 (and all subsequent releases and versions).
### 1. Preamble
The Licensed Software is designed to facilitate the remote monitoring and management (RMM) of networks, systems, servers, computers and other devices. The Licensed Software is made available primarily for use by organisations and managed service providers for monitoring and management purposes.
The Tactical RMM License is not an open-source software license. This license contains certain restrictions on the use of the Licensed Software. For example the functionality of the Licensed Software may not be made available as part of a SaaS (Software-as-a-Service) service or product to provide a commercial or for-profit service without the express prior permission of the Licensor.
### 2. License Grant
Permission is hereby granted, free of charge, on a non-exclusive basis, to copy, modify, create derivative works and use the Licensed Software in source and binary forms subject to the following terms and conditions. No additional rights will be implied under this license.
* The hosting and use of the Licensed Software to monitor and manage in-house networks/systems and/or customer networks/systems is permitted.
This license does not allow the functionality of the Licensed Software (whether in whole or in part) or a modified version of the Licensed Software or a derivative work to be used or otherwise made available as part of any other commercial or for-profit service, including, without limitation, any of the following:
* a service allowing third parties to interact remotely through a computer network;
* as part of a SaaS service or product;
* as part of the provision of a managed hosting service or product;
* the offering of installation and/or configuration services;
* the offer for sale, distribution or sale of any service or product (whether or not branded as Tactical RMM).
The prior written approval of AmidaWare LLC must be obtained for all commercial use and/or for-profit service use of the (i) Licensed Software (whether in whole or in part), (ii) a modified version of the Licensed Software and/or (iii) a derivative work.
The terms of this license apply to all copies of the Licensed Software (including modified versions) and derivative works.
All use of the Licensed Software must immediately cease if use breaches the terms of this license.
### 3. Derivative Works
If a derivative work is created which is based on or otherwise incorporates all or any part of the Licensed Software, and the derivative work is made available to any other person, the complete corresponding machine readable source code (including all changes made to the Licensed Software) must accompany the derivative work and be made publicly available online.
### 4. Copyright Notice
The following copyright notice shall be included in all copies of the Licensed Software:
&emsp;&emsp;&emsp;Copyright © 2022 AmidaWare LLC.
&emsp;&emsp;&emsp;Licensed under the Tactical RMM License Version 1.0 (the “License”).<br>
&emsp;&emsp;&emsp;You may only use the Licensed Software in accordance with the License.<br>
&emsp;&emsp;&emsp;A copy of the License is available at: https://license.tacticalrmm.com
### 5. Disclaimer of Warranty
THE LICENSED SOFTWARE IS PROVIDED "AS IS". TO THE FULLEST EXTENT PERMISSIBLE AT LAW ALL CONDITIONS, WARRANTIES OR OTHER TERMS OF ANY KIND WHICH MIGHT HAVE EFFECT OR BE IMPLIED OR INCORPORATED, WHETHER BY STATUTE, COMMON LAW OR OTHERWISE ARE HEREBY EXCLUDED, INCLUDING THE CONDITIONS, WARRANTIES OR OTHER TERMS AS TO SATISFACTORY QUALITY AND/OR MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, THE USE OF REASONABLE SKILL AND CARE AND NON-INFRINGEMENT.
### 6. Limits of Liability
THE FOLLOWING EXCLUSIONS SHALL APPLY TO THE FULLEST EXTENT PERMISSIBLE AT LAW. NEITHER THE AUTHORS NOR THE COPYRIGHT HOLDERS SHALL IN ANY CIRCUMSTANCES HAVE ANY LIABILITY FOR ANY CLAIM, LOSSES, DAMAGES OR OTHER LIABILITY, WHETHER THE SAME ARE SUFFERED DIRECTLY OR INDIRECTLY OR ARE IMMEDIATE OR CONSEQUENTIAL, AND WHETHER THE SAME ARISE IN CONTRACT, TORT OR DELICT (INCLUDING NEGLIGENCE) OR OTHERWISE HOWSOEVER ARISING FROM, OUT OF OR IN CONNECTION WITH THE LICENSED SOFTWARE OR THE USE OR INABILITY TO USE THE LICENSED SOFTWARE OR OTHER DEALINGS IN THE LICENSED SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH LOSS OR DAMAGE. THE FOREGOING EXCLUSIONS SHALL INCLUDE, WITHOUT LIMITATION, LIABILITY FOR ANY LOSSES OR DAMAGES WHICH FALL WITHIN ANY OF THE FOLLOWING CATEGORIES: SPECIAL, EXEMPLARY, OR INCIDENTAL LOSS OR DAMAGE, LOSS OF PROFITS, LOSS OF ANTICIPATED SAVINGS, LOSS OF BUSINESS OPPORTUNITY, LOSS OF GOODWILL, AND LOSS OR CORRUPTION OF DATA.
### 7. Termination
This license shall terminate with immediate effect if there is a material breach of any of its terms.
### 8. No partnership, agency or joint venture
Nothing in this license agreement is intended to, or shall be deemed to, establish any partnership or joint venture or any relationship of agency between AmidaWare LLC and any other person.
### 9. No endorsement
The names of the authors and/or the copyright holders must not be used to promote or endorse any products or services which are in any way derived from the Licensed Software without prior written consent.
### 10. Trademarks
No permission is granted to use the trademark “Tactical RMM” or any other trade name, trademark, service mark or product name of AmidaWare LLC except to the extent necessary to comply with the notice requirements in Section 4 (Copyright Notice).
### 11. Entire agreement
This license contains the whole agreement relating to its subject matter.
### 12. Severance
If any provision or part-provision of this license is or becomes invalid, illegal or unenforceable, it shall be deemed deleted, but that shall not affect the validity and enforceability of the rest of this license.
### 13. Acceptance of these terms
The terms and conditions of this license are accepted by copying, downloading, installing, redistributing, or otherwise using the Licensed Software.

2
go.mod
View File

@ -9,7 +9,7 @@ require (
github.com/nats-io/nats-server/v2 v2.4.0 // indirect
github.com/nats-io/nats.go v1.13.0
github.com/ugorji/go/codec v1.2.6
github.com/wh1te909/trmm-shared v0.0.0-20211112185254-e9c45faf2b83
github.com/wh1te909/trmm-shared v0.0.0-20220227075846-f9f757361139
google.golang.org/protobuf v1.27.1 // indirect
)

2
go.sum
View File

@ -64,6 +64,8 @@ github.com/wh1te909/trmm-shared v0.0.0-20211111193154-6d7f8e4d0dcd h1:18S4tn72OO
github.com/wh1te909/trmm-shared v0.0.0-20211111193154-6d7f8e4d0dcd/go.mod h1:ILUz1utl5KgwrxmNHv0RpgMtKeh8gPAABvK2MiXBqv8=
github.com/wh1te909/trmm-shared v0.0.0-20211112185254-e9c45faf2b83 h1:faCwMxF0DwMppqThweKdmoxfruB/C/NjTYDG5d9O5V4=
github.com/wh1te909/trmm-shared v0.0.0-20211112185254-e9c45faf2b83/go.mod h1:ILUz1utl5KgwrxmNHv0RpgMtKeh8gPAABvK2MiXBqv8=
github.com/wh1te909/trmm-shared v0.0.0-20220227075846-f9f757361139 h1:PfOl03o+Y+svWrfXAAu1QWUDePu1yqTq0pf4rpnN8eA=
github.com/wh1te909/trmm-shared v0.0.0-20220227075846-f9f757361139/go.mod h1:ILUz1utl5KgwrxmNHv0RpgMtKeh8gPAABvK2MiXBqv8=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210314154223-e6e6c4f2bb5b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=

View File

@ -1,6 +1,6 @@
#!/bin/bash
SCRIPT_VERSION="58"
SCRIPT_VERSION="59"
SCRIPT_URL='https://raw.githubusercontent.com/wh1te909/tacticalrmm/master/install.sh'
sudo apt install -y curl wget dirmngr gnupg lsb-release
@ -12,6 +12,7 @@ RED='\033[0;31m'
NC='\033[0m'
SCRIPTS_DIR="/opt/trmm-community-scripts"
PYTHON_VER="3.10.2"
TMP_FILE=$(mktemp -p "" "rmminstall_XXXXXXXXXX")
curl -s -L "${SCRIPT_URL}" > ${TMP_FILE}
@ -177,7 +178,7 @@ sudo sed -i 's/# server_names_hash_bucket_size.*/server_names_hash_bucket_size 6
print_green 'Installing NodeJS'
curl -sL https://deb.nodesource.com/setup_14.x | sudo -E bash -
curl -sL https://deb.nodesource.com/setup_16.x | sudo -E bash -
sudo apt update
sudo apt install -y gcc g++ make
sudo apt install -y nodejs
@ -192,19 +193,19 @@ sudo apt install -y mongodb-org
sudo systemctl enable mongod
sudo systemctl restart mongod
print_green 'Installing Python 3.9'
print_green 'Installing Python 3.10.2'
sudo apt install -y build-essential zlib1g-dev libncurses5-dev libgdbm-dev libnss3-dev libssl-dev libreadline-dev libffi-dev libsqlite3-dev libbz2-dev
numprocs=$(nproc)
cd ~
wget https://www.python.org/ftp/python/3.9.9/Python-3.9.9.tgz
tar -xf Python-3.9.9.tgz
cd Python-3.9.9
wget https://www.python.org/ftp/python/${PYTHON_VER}/Python-${PYTHON_VER}.tgz
tar -xf Python-${PYTHON_VER}.tgz
cd Python-${PYTHON_VER}
./configure --enable-optimizations
make -j $numprocs
sudo make altinstall
cd ~
sudo rm -rf Python-3.9.9 Python-3.9.9.tgz
sudo rm -rf Python-${PYTHON_VER} Python-${PYTHON_VER}.tgz
print_green 'Installing redis and git'
@ -220,7 +221,7 @@ echo "$postgresql_repo" | sudo tee /etc/apt/sources.list.d/pgdg.list
wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | sudo apt-key add -
sudo apt update
sudo apt install -y postgresql-13
sudo apt install -y postgresql-14
sleep 2
sudo systemctl enable postgresql
sudo systemctl restart postgresql
@ -355,7 +356,7 @@ SETUPTOOLS_VER=$(grep "^SETUPTOOLS_VER" /rmm/api/tacticalrmm/tacticalrmm/setting
WHEEL_VER=$(grep "^WHEEL_VER" /rmm/api/tacticalrmm/tacticalrmm/settings.py | awk -F'[= "]' '{print $5}')
cd /rmm/api
python3.9 -m venv env
python3.10 -m venv env
source /rmm/api/env/bin/activate
cd /rmm/api/tacticalrmm
pip install --no-cache-dir --upgrade pip
@ -796,11 +797,11 @@ echo "${meshtoken}" | tee --append /rmm/api/tacticalrmm/tacticalrmm/local_settin
print_green 'Creating meshcentral account and group'
sudo systemctl stop meshcentral
sleep 3
sleep 1
cd /meshcentral
node node_modules/meshcentral --createaccount ${meshusername} --pass ${MESHPASSWD} --email ${letsemail}
sleep 2
sleep 1
node node_modules/meshcentral --adminaccount ${meshusername}
sudo systemctl start meshcentral
@ -813,8 +814,7 @@ while ! [[ $CHECK_MESH_READY2 ]]; do
done
node node_modules/meshcentral/meshctrl.js --url wss://${meshdomain}:443 --loginuser ${meshusername} --loginpass ${MESHPASSWD} AddDeviceGroup --name TacticalRMM
sleep 5
MESHEXE=$(node node_modules/meshcentral/meshctrl.js --url wss://${meshdomain}:443 --loginuser ${meshusername} --loginpass ${MESHPASSWD} GenerateInviteLink --group TacticalRMM --hours 8)
sleep 1
sudo systemctl enable nats.service
cd /rmm/api/tacticalrmm
@ -841,9 +841,6 @@ done
printf >&2 "${YELLOW}%0.s*${NC}" {1..80}
printf >&2 "\n\n"
printf >&2 "${YELLOW}Installation complete!${NC}\n\n"
printf >&2 "${YELLOW}Download the meshagent 64 bit EXE from:\n\n${GREEN}"
echo ${MESHEXE} | sed 's/{.*}//'
printf >&2 "${NC}\n\n"
printf >&2 "${YELLOW}Access your rmm at: ${GREEN}https://${frontenddomain}${NC}\n\n"
printf >&2 "${YELLOW}Django admin url: ${GREEN}https://${rmmdomain}/${ADMINURL}${NC}\n\n"
printf >&2 "${YELLOW}MeshCentral username: ${GREEN}${meshusername}${NC}\n"

View File

@ -11,7 +11,7 @@ import (
)
var (
version = "3.0.0"
version = "3.0.1"
log = logrus.New()
)

Binary file not shown.

View File

@ -83,6 +83,15 @@ func Svc(logger *logrus.Logger, cfg string) {
logger.Errorln(err)
}
// TODO add this to main stmt once agent 2.0.0 has been out for a while
if r.GoArch != "" {
stmt = `UPDATE agents_agent SET goarch=$1 WHERE agents_agent.agent_id=$2;`
_, err = db.Exec(stmt, r.GoArch, r.Agentid)
if err != nil {
logger.Errorln(err)
}
}
if r.Username != "None" {
stmt = `UPDATE agents_agent SET last_logged_in_user=$1 WHERE agents_agent.agent_id=$2;`
logger.Debugln("Updating last logged in user:", r.Username)

View File

@ -1,6 +1,6 @@
#!/bin/bash
SCRIPT_VERSION="33"
SCRIPT_VERSION="34"
SCRIPT_URL='https://raw.githubusercontent.com/wh1te909/tacticalrmm/master/restore.sh'
sudo apt update
@ -13,6 +13,7 @@ RED='\033[0;31m'
NC='\033[0m'
SCRIPTS_DIR="/opt/trmm-community-scripts"
PYTHON_VER="3.10.2"
TMP_FILE=$(mktemp -p "" "rmmrestore_XXXXXXXXXX")
curl -s -L "${SCRIPT_URL}" > ${TMP_FILE}
@ -111,7 +112,7 @@ sudo apt update
print_green 'Installing NodeJS'
curl -sL https://deb.nodesource.com/setup_14.x | sudo -E bash -
curl -sL https://deb.nodesource.com/setup_16.x | sudo -E bash -
sudo apt update
sudo apt install -y gcc g++ make
sudo apt install -y nodejs
@ -163,19 +164,19 @@ print_green 'Restoring systemd services'
sudo cp $tmp_dir/systemd/* /etc/systemd/system/
sudo systemctl daemon-reload
print_green 'Installing Python 3.9'
print_green 'Installing Python 3.10.2'
sudo apt install -y build-essential zlib1g-dev libncurses5-dev libgdbm-dev libnss3-dev libssl-dev libreadline-dev libffi-dev libsqlite3-dev libbz2-dev
numprocs=$(nproc)
cd ~
wget https://www.python.org/ftp/python/3.9.9/Python-3.9.9.tgz
tar -xf Python-3.9.9.tgz
cd Python-3.9.9
wget https://www.python.org/ftp/python/${PYTHON_VER}/Python-${PYTHON_VER}.tgz
tar -xf Python-${PYTHON_VER}.tgz
cd Python-${PYTHON_VER}
./configure --enable-optimizations
make -j $numprocs
sudo make altinstall
cd ~
sudo rm -rf Python-3.9.9 Python-3.9.9.tgz
sudo rm -rf Python-${PYTHON_VER} Python-${PYTHON_VER}.tgz
print_green 'Installing redis and git'
@ -193,7 +194,7 @@ print_green 'Installing postgresql'
echo "$postgresql_repo" | sudo tee /etc/apt/sources.list.d/pgdg.list
wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | sudo apt-key add -
sudo apt update
sudo apt install -y postgresql-13
sudo apt install -y postgresql-14
sleep 2
sudo systemctl enable postgresql
sudo systemctl restart postgresql
@ -308,7 +309,7 @@ SETUPTOOLS_VER=$(grep "^SETUPTOOLS_VER" /rmm/api/tacticalrmm/tacticalrmm/setting
WHEEL_VER=$(grep "^WHEEL_VER" /rmm/api/tacticalrmm/tacticalrmm/settings.py | awk -F'[= "]' '{print $5}')
cd /rmm/api
python3.9 -m venv env
python3.10 -m venv env
source /rmm/api/env/bin/activate
cd /rmm/api/tacticalrmm
pip install --no-cache-dir --upgrade pip

14
web/package-lock.json generated
View File

@ -9,7 +9,7 @@
"version": "0.1.8",
"dependencies": {
"@quasar/extras": "^1.12.5",
"apexcharts": "^3.33.1",
"apexcharts": "^3.33.2",
"axios": "^0.26.0",
"dotenv": "^16.0.0",
"qrcode.vue": "^3.3.3",
@ -3077,9 +3077,9 @@
}
},
"node_modules/apexcharts": {
"version": "3.33.1",
"resolved": "https://registry.npmjs.org/apexcharts/-/apexcharts-3.33.1.tgz",
"integrity": "sha512-5aVzrgJefd8EH4w7oRmuOhA3+cxJxQg27cYg3ANVGvPCOB4AY3mVVNtFHRFaIq7bv8ws4GRaA9MWfzoWQw3MPQ==",
"version": "3.33.2",
"resolved": "https://registry.npmjs.org/apexcharts/-/apexcharts-3.33.2.tgz",
"integrity": "sha512-GkHZ3o36ZT/jSBh5y1pxxRzwM3tvtladtkcUTfXwP0wYAHK8Qj0X4ZPsupP7emRIjhOVpGsCxW9xeO3F5w+AOQ==",
"dependencies": {
"svg.draggable.js": "^2.2.2",
"svg.easing.js": "^2.0.0",
@ -15286,9 +15286,9 @@
}
},
"apexcharts": {
"version": "3.33.1",
"resolved": "https://registry.npmjs.org/apexcharts/-/apexcharts-3.33.1.tgz",
"integrity": "sha512-5aVzrgJefd8EH4w7oRmuOhA3+cxJxQg27cYg3ANVGvPCOB4AY3mVVNtFHRFaIq7bv8ws4GRaA9MWfzoWQw3MPQ==",
"version": "3.33.2",
"resolved": "https://registry.npmjs.org/apexcharts/-/apexcharts-3.33.2.tgz",
"integrity": "sha512-GkHZ3o36ZT/jSBh5y1pxxRzwM3tvtladtkcUTfXwP0wYAHK8Qj0X4ZPsupP7emRIjhOVpGsCxW9xeO3F5w+AOQ==",
"requires": {
"svg.draggable.js": "^2.2.2",
"svg.easing.js": "^2.0.0",

View File

@ -11,7 +11,7 @@
},
"dependencies": {
"@quasar/extras": "^1.12.5",
"apexcharts": "^3.33.1",
"apexcharts": "^3.33.2",
"axios": "^0.26.0",
"dotenv": "^16.0.0",
"qrcode.vue": "^3.3.3",

View File

@ -14,8 +14,8 @@ export function openAgentWindow(agent_id) {
openURL(url, null, { popup: true, scrollbars: false, location: false, status: false, toolbar: false, menubar: false, width: 1600, height: 900 });
}
export function runRemoteBackground(agent_id) {
const url = router.resolve(`/remotebackground/${agent_id}`).href;
export function runRemoteBackground(agent_id, agentPlatform) {
const url = router.resolve(`/remotebackground/${agent_id}?agentPlatform=${agentPlatform}`).href;
openURL(url, null, { popup: true, scrollbars: false, location: false, status: false, toolbar: false, menubar: false, width: 1280, height: 900 });
}

View File

@ -10,11 +10,6 @@ export async function fetchCustomFields(params = {}) {
} catch (e) { console.error(e) }
}
export async function uploadMeshAgent(payload) {
const { data } = await axios.put(`${baseUrl}/uploadmesh/`, payload)
return data
}
export async function fetchDashboardInfo(params = {}) {
const { data } = await axios.get(`${baseUrl}/dashinfo/`, { params: params })
return data

View File

@ -40,6 +40,9 @@
</q-icon>
</q-th>
</template>
<template v-slot:header-cell-plat="props">
<q-th auto-width :props="props"></q-th>
</template>
<template v-slot:header-cell-checks-status="props">
<q-th :props="props">
<q-icon name="fas fa-check-double" size="1.2em">
@ -78,10 +81,10 @@
<!-- body slots -->
<template v-slot:body="props">
<q-tr
@contextmenu="agentRowSelected(props.row.agent_id)"
@contextmenu="agentRowSelected(props.row.agent_id, props.row.plat)"
:props="props"
:class="rowSelectedClass(props.row.agent_id)"
@click="agentRowSelected(props.row.agent_id)"
@click="agentRowSelected(props.row.agent_id, props.row.plat)"
@dblclick="rowDoubleClicked(props.row.agent_id)"
>
<q-menu context-menu>
@ -138,6 +141,16 @@
v-model="props.row.overdue_dashboard_alert"
/>
</q-td>
<q-td key="plat" :props="props">
<q-icon v-if="props.row.plat === 'windows'" name="mdi-microsoft-windows" size="sm" color="primary">
<q-tooltip>Microsoft Windows</q-tooltip>
</q-icon>
<q-icon v-else-if="props.row.plat === 'linux'" name="mdi-linux" size="sm" color="primary">
<q-tooltip>Linux</q-tooltip>
</q-icon>
</q-td>
<q-td key="checks-status" :props="props">
<q-icon v-if="props.row.maintenance_mode" name="construction" size="1.2em" color="green">
<q-tooltip>Maintenance Mode Enabled</q-tooltip>
@ -155,9 +168,9 @@
<q-tooltip>Checks passing</q-tooltip>
</q-icon>
</q-td>
<q-td key="client_name" :props="props">{{ props.row.client_name }}</q-td>
<q-td key="site_name" :props="props">{{ props.row.site_name }}</q-td>
<q-td key="hostname" :props="props">{{ props.row.hostname }}</q-td>
<q-td key="description" :props="props">{{ props.row.description }}</q-td>
<q-td key="user" :props="props">
@ -286,7 +299,7 @@ export default {
});
});
},
rowDoubleClicked(agent_id) {
rowDoubleClicked(agent_id, agentPlatform) {
this.$store.commit("setActiveRow", agent_id);
this.$q.loading.show();
// give time for store to change active row
@ -300,7 +313,7 @@ export default {
runTakeControl(agent_id);
break;
case "remotebg":
runRemoteBackground(agent_id);
runRemoteBackground(agent_id, agentPlatform);
break;
case "urlaction":
runURLAction({ agent_id: agent_id, action: this.agentUrlAction });
@ -316,8 +329,9 @@ export default {
},
});
},
agentRowSelected(agent_id) {
agentRowSelected(agent_id, agentPlatform) {
this.$store.commit("setActiveRow", agent_id);
this.$store.commit("setAgentPlatform", agentPlatform);
},
overdueAlert(category, agent, alert_action) {
let db_field = "";

View File

@ -85,7 +85,7 @@
</q-menu>
</q-item>
<q-item clickable v-close-popup @click="runRemoteBackground(agent.agent_id)">
<q-item clickable v-close-popup @click="runRemoteBackground(agent.agent_id, agent.plat)">
<q-item-section side>
<q-icon size="xs" name="fas fa-cogs" />
</q-item-section>
@ -185,7 +185,6 @@
<script>
// composition imports
import { ref, inject } from "vue";
import { useStore } from "vuex";
import { useQuasar } from "quasar";
import { fetchURLActions, runURLAction } from "@/api/core";
import {
@ -217,7 +216,6 @@ export default {
},
setup(props) {
const $q = useQuasar();
const store = useStore();
const refreshDashboard = inject("refreshDashboard");

View File

@ -1,5 +1,8 @@
<template>
<div v-if="!selectedAgent" class="q-pa-sm">No agent selected</div>
<div v-else-if="agentPlatform.toLowerCase() !== 'windows'" class="q-pa-sm">
Only supported for Windows agents at this time
</div>
<div v-else>
<q-tabs
v-model="tab"
@ -88,6 +91,7 @@ export default {
// setup vuex
const store = useStore();
const selectedAgent = computed(() => store.state.selectedRow);
const agentPlatform = computed(() => store.state.agentPlatform);
const loading = ref(false);
// assets tab logic
@ -116,6 +120,7 @@ export default {
assets,
tab,
selectedAgent,
agentPlatform,
};
},
};

View File

@ -1,5 +1,8 @@
<template>
<div v-if="!selectedAgent" class="q-pa-sm">No agent selected</div>
<div v-else-if="agentPlatform.toLowerCase() !== 'windows'" class="q-pa-sm">
Only supported for Windows agents at this time
</div>
<div v-else>
<q-table
dense
@ -300,6 +303,7 @@ export default {
const store = useStore();
const selectedAgent = computed(() => store.state.selectedRow);
const tabHeight = computed(() => store.state.tabHeight);
const agentPlatform = computed(() => store.state.agentPlatform);
// setup quasar
const $q = useQuasar();
@ -428,6 +432,7 @@ export default {
pagination,
selectedAgent,
tabHeight,
agentPlatform,
// non-reactive data
columns,

View File

@ -25,7 +25,7 @@
<q-btn class="q-mr-sm" dense flat push @click="getChecks" icon="refresh" />
<q-btn-dropdown icon="add" label="New" no-caps dense flat>
<q-list dense style="min-width: 200px">
<q-item clickable v-close-popup @click="showCheckModal('diskspace')">
<q-item v-if="agentPlatform === 'windows'" clickable v-close-popup @click="showCheckModal('diskspace')">
<q-item-section side>
<q-icon size="xs" name="far fa-hdd" />
</q-item-section>
@ -37,19 +37,19 @@
</q-item-section>
<q-item-section>Ping Check</q-item-section>
</q-item>
<q-item clickable v-close-popup @click="showCheckModal('cpuload')">
<q-item v-if="agentPlatform === 'windows'" clickable v-close-popup @click="showCheckModal('cpuload')">
<q-item-section side>
<q-icon size="xs" name="fas fa-microchip" />
</q-item-section>
<q-item-section>CPU Load Check</q-item-section>
</q-item>
<q-item clickable v-close-popup @click="showCheckModal('memory')">
<q-item v-if="agentPlatform === 'windows'" clickable v-close-popup @click="showCheckModal('memory')">
<q-item-section side>
<q-icon size="xs" name="fas fa-memory" />
</q-item-section>
<q-item-section>Memory Check</q-item-section>
</q-item>
<q-item clickable v-close-popup @click="showCheckModal('winsvc')">
<q-item v-if="agentPlatform === 'windows'" clickable v-close-popup @click="showCheckModal('winsvc')">
<q-item-section side>
<q-icon size="xs" name="fas fa-cogs" />
</q-item-section>
@ -61,7 +61,7 @@
</q-item-section>
<q-item-section>Script Check</q-item-section>
</q-item>
<q-item clickable v-close-popup @click="showCheckModal('eventlog')">
<q-item v-if="agentPlatform === 'windows'" clickable v-close-popup @click="showCheckModal('eventlog')">
<q-item-section side>
<q-icon size="xs" name="fas fa-clipboard-list" />
</q-item-section>
@ -336,6 +336,7 @@ export default {
const store = useStore();
const selectedAgent = computed(() => store.state.selectedRow);
const tabHeight = computed(() => store.state.tabHeight);
const agentPlatform = computed(() => store.state.agentPlatform);
// setup quasar
const $q = useQuasar();
@ -482,6 +483,7 @@ export default {
pagination,
tabHeight,
selectedAgent,
agentPlatform,
// non-reactive data
columns,

View File

@ -1,5 +1,8 @@
<template>
<div v-if="!selectedAgent" class="q-pa-sm">No agent selected</div>
<div v-else-if="agentPlatform.toLowerCase() !== 'windows'" class="q-pa-sm">
Only supported for Windows agents at this time
</div>
<div v-else>
<q-table
:table-class="{ 'table-bgcolor': !$q.dark.isActive, 'table-bgcolor-dark': $q.dark.isActive }"
@ -103,6 +106,7 @@ export default {
const store = useStore();
const selectedAgent = computed(() => store.state.selectedRow);
const tabHeight = computed(() => store.state.tabHeight);
const agentPlatform = computed(() => store.state.agentPlatform);
// software tab logic
const software = ref([]);
@ -154,6 +158,7 @@ export default {
pagination,
selectedAgent,
tabHeight,
agentPlatform,
// non-reactive data
columns,

View File

@ -1,5 +1,8 @@
<template>
<div v-if="!selectedAgent" class="q-pa-sm">No agent selected</div>
<div v-else-if="agentPlatform.toLowerCase() !== 'windows'" class="q-pa-sm">
Only supported for Windows agents at this time
</div>
<div v-else>
<q-table
dense
@ -179,6 +182,7 @@ export default {
const store = useStore();
const selectedAgent = computed(() => store.state.selectedRow);
const tabHeight = computed(() => store.state.tabHeight);
const agentPlatform = computed(() => store.state.agentPlatform);
// setup quasar
const $q = useQuasar();
@ -274,6 +278,7 @@ export default {
loading,
selectedAgent,
tabHeight,
agentPlatform,
// non-reactive data
columns,

View File

@ -1,5 +1,8 @@
<template>
<div>
<div v-if="agentPlatform.toLowerCase() !== 'windows'" class="q-pa-sm">
Only supported for Windows agents at this time
</div>
<div v-else>
<div class="row q-pt-sm q-pl-sm">
<div class="col-2">
<q-select dense options-dense outlined v-model="days" :options="lastDaysOptions" :label="showDays" />
@ -90,6 +93,7 @@ export default {
},
props: {
agent_id: !String,
agentPlatform: !String,
},
setup(props) {
// quasar setup
@ -121,7 +125,9 @@ export default {
}
// vue lifecycle hooks
onMounted(getEventLog);
onMounted(() => {
if (props.agentPlatform === "windows") getEventLog();
});
return {
// reactive data

View File

@ -1,5 +1,9 @@
<template>
<div v-if="agentPlatform.toLowerCase() !== 'windows'" class="q-pa-sm">
Only supported for Windows agents at this time
</div>
<q-table
v-else
dense
:table-class="{ 'table-bgcolor': !$q.dark.isActive, 'table-bgcolor-dark': $q.dark.isActive }"
class="remote-bg-tbl-sticky"
@ -150,6 +154,7 @@ export default {
},
props: {
agent_id: !String,
agentPlatform: !String,
},
setup(props) {
// quasar setup
@ -190,8 +195,9 @@ export default {
}
// vue lifecycle hooks
onMounted(getServices);
onMounted(() => {
if (props.agentPlatform === "windows") getServices();
});
return {
// reactive data
services,

View File

@ -1,98 +0,0 @@
<template>
<q-dialog ref="dialogRef" @hide="onDialogHide">
<q-card class="q-dialog-plugin" style="width: 40vw">
<q-bar>
Upload Mesh Exe
<q-space />
<q-btn dense flat icon="close" v-close-popup>
<q-tooltip class="bg-white text-primary">Close</q-tooltip>
</q-btn>
</q-bar>
<q-form @submit.prevent="submitForm">
<q-card-section>
<div class="q-gutter-sm">
<q-radio v-model="form.arch" val="64" label="64 bit" />
<q-radio v-model="form.arch" val="32" label="32 bit" />
</div>
</q-card-section>
<q-card-section>
<q-file
v-model="form.meshagent"
:rules="[val => !!val || '*Required']"
label="Upload MeshAgent"
stack-label
filled
counter
class="full-width"
accept=".exe"
>
<template v-slot:prepend>
<q-icon name="attach_file" />
</template>
</q-file>
</q-card-section>
<q-card-actions>
<q-space />
<q-btn dense flat label="Cancel" v-close-popup />
<q-btn :loading="loading" dense flat label="Upload" color="primary" type="submit" />
</q-card-actions>
</q-form>
</q-card>
</q-dialog>
</template>
<script>
// composition imports
import { ref } from "vue";
import { useDialogPluginComponent } from "quasar";
import { uploadMeshAgent } from "@/api/core";
import { notifySuccess } from "@/utils/notify";
export default {
name: "UploadMesh",
emits: [...useDialogPluginComponent.emits],
setup(props) {
// setup quasar plugins
const { dialogRef, onDialogHide, onDialogOK } = useDialogPluginComponent();
// upload mesh logic
const form = ref({
meshagent: null,
arch: "64",
});
const loading = ref(false);
async function submitForm() {
loading.value = true;
let result = "";
let formData = new FormData();
formData.append("arch", form.value.arch);
formData.append("meshagent", form.value.meshagent);
try {
result = await uploadMeshAgent(formData);
onDialogOK();
notifySuccess(result);
} catch (e) {
console.error(e);
}
loading.value = false;
}
return {
// reactive data
form,
loading,
//methods
submitForm,
// quasar dialog
dialogRef,
onDialogHide,
};
},
};
</script>

View File

@ -56,9 +56,6 @@
<q-td v-if="props.row.action_type === 'schedreboot'">
<q-icon name="power_settings_new" size="sm" />
</q-td>
<q-td v-else-if="props.row.action_type === 'taskaction'">
<q-icon name="fas fa-tasks" size="sm" />
</q-td>
<q-td v-else-if="props.row.action_type === 'agentupdate'">
<q-icon name="update" size="sm" />
</q-td>

View File

@ -40,12 +40,7 @@
<q-badge class="text-caption q-mr-xs" color="grey" text-color="black">
<code>-local-mesh "C:\\&lt;some folder or path&gt;\\meshagent.exe"</code>
</q-badge>
<span>
To skip downloading the Mesh Agent during the install. Download it
<span style="cursor: pointer; text-decoration: underline" class="text-primary" @click="downloadMesh"
>here</span
>
</span>
<span> To skip downloading the Mesh Agent during the install.</span>
</div>
<div class="q-pa-xs q-gutter-xs">
<q-badge class="text-caption q-mr-xs" color="grey" text-color="black">
@ -92,20 +87,5 @@ export default {
name: "AgentDownload",
mixins: [mixins],
props: ["info"],
methods: {
downloadMesh() {
const fileName = this.info.arch === "64" ? "meshagent.exe" : "meshagent-x86.exe";
this.$axios
.post(`/agents/${this.info.arch}/getmeshexe/`, {}, { responseType: "blob" })
.then(({ data }) => {
const blob = new Blob([data], { type: "application/vnd.microsoft.portable-executable" });
let link = document.createElement("a");
link.href = window.URL.createObjectURL(blob);
link.download = fileName;
link.click();
})
.catch(e => {});
},
},
};
</script>

View File

@ -12,7 +12,6 @@
<q-card-section>
<div class="q-gutter-sm">
<q-radio dense v-model="state.mode" val="mesh" label="Mesh Agent" />
<q-radio dense v-model="state.mode" val="rpc" label="Tactical RPC" />
<q-radio dense v-model="state.mode" val="tacagent" label="Tactical Agent" />
<q-radio dense v-model="state.mode" val="command" label="Shell Command" />
</div>
@ -21,16 +20,12 @@
Fix issues with the Mesh Agent which handles take control, live terminal and file browser.
</q-card-section>
<q-card-section v-else-if="state.mode === 'tacagent'">
Fix issues with the TacticalAgent windows service which handles agent check-in.
</q-card-section>
<q-card-section v-else-if="state.mode === 'rpc'">
Fix issues with the Tactical RPC service which handles most of the agent's realtime functions and scheduled
tasks.
Fix issues with the Tactical RMM Agent windows service.
</q-card-section>
<q-card-section v-else-if="state.mode === 'command'">
<p>Run a shell command on the agent.</p>
<p>You should use the 'Send Command' feature from the agent's context menu for sending shell commands.</p>
<p>Only use this as a last resort if unable to recover the Tactical RPC service.</p>
<p>Only use this as a last resort if unable to recover the Tactical RMM Agent service.</p>
<q-input
ref="input"
v-model="state.cmd"

View File

@ -55,6 +55,18 @@
/>
</q-card-section>
<q-card-section>
<p>Agent OS</p>
<q-option-group
v-model="state.osType"
:options="osTypeOptions"
color="primary"
dense
inline
class="q-pl-sm"
/>
</q-card-section>
<q-card-section v-show="state.target !== 'agents'">
<p>Agent Type</p>
<q-option-group
@ -71,7 +83,7 @@
<tactical-dropdown
:rules="[val => !!val || '*Required']"
v-model="state.script"
:options="scriptOptions"
:options="filteredScriptOptions"
label="Select Script"
outlined
mapOptions
@ -93,7 +105,25 @@
<q-card-section v-if="mode === 'command'">
<p>Shell</p>
<q-option-group v-model="state.shell" :options="shellOptions" color="primary" dense inline class="q-pl-sm" />
<q-option-group
v-model="state.shell"
:options="shellOptions"
color="primary"
dense
inline
class="q-pl-sm"
@update:model-value="state.custom_shell = null"
/>
</q-card-section>
<q-card-section v-if="state.shell === 'custom'">
<q-input
v-model="state.custom_shell"
outlined
label="Custom shell"
stack-label
placeholder="/usr/bin/python3"
:rules="[val => !!val || '*Required']"
/>
</q-card-section>
<q-card-section v-if="mode === 'command'">
<q-input
@ -101,11 +131,7 @@
outlined
label="Command"
stack-label
:placeholder="
state.shell === 'cmd'
? 'rmdir /S /Q C:\\Windows\\System32'
: 'Remove-Item -Recurse -Force C:\\Windows\\System32'
"
:placeholder="cmdPlaceholder(state.shell)"
:rules="[val => !!val || '*Required']"
/>
</q-card-section>
@ -160,6 +186,8 @@ import { useAgentDropdown } from "@/composables/agents";
import { useClientDropdown, useSiteDropdown } from "@/composables/clients";
import { runBulkAction } from "@/api/agents";
import { notifySuccess } from "@/utils/notify";
import { cmdPlaceholder } from "@/composables/agents";
import { removeExtraOptionCategories } from "@/utils/format";
// ui imports
import TacticalDropdown from "@/components/ui/TacticalDropdown";
@ -171,6 +199,11 @@ const monTypeOptions = [
{ label: "Workstations", value: "workstations" },
];
const osTypeOptions = [
{ label: "Windows", value: "windows" },
{ label: "Linux", value: "linux" },
];
const targetOptions = [
{ label: "Client", value: "client" },
{ label: "Site", value: "site" },
@ -178,11 +211,6 @@ const targetOptions = [
{ label: "All", value: "all" },
];
const shellOptions = [
{ label: "CMD", value: "cmd" },
{ label: "Powershell", value: "powershell" },
];
const patchModeOptions = [
{ label: "Scan", value: "scan" },
{ label: "Install", value: "install" },
@ -200,6 +228,20 @@ export default {
const store = useStore();
const showCommunityScripts = computed(() => store.state.showCommunityScripts);
const shellOptions = computed(() => {
if (state.value.osType === "windows") {
return [
{ label: "CMD", value: "cmd" },
{ label: "Powershell", value: "powershell" },
];
} else {
return [
{ label: "Bash", value: "/bin/bash" },
{ label: "Custom", value: "custom" },
];
}
});
// quasar dialog setup
const { dialogRef, onDialogHide } = useDialogPluginComponent();
@ -214,8 +256,10 @@ export default {
mode: props.mode,
target: "client",
monType: "all",
osType: "windows",
cmd: "",
shell: "cmd",
custom_shell: null,
patchMode: "scan",
offlineAgents: false,
client,
@ -236,6 +280,19 @@ export default {
}
);
watch(
() => state.value.osType,
(newValue, oldValue) => {
state.value.custom_shell = null;
if (newValue === "windows") {
state.value.shell = "cmd";
} else {
state.value.shell = "/bin/bash";
}
}
);
async function submit() {
loading.value = true;
@ -259,6 +316,19 @@ export default {
: "";
});
const filteredScriptOptions = computed(() => {
if (props.mode !== "script") return [];
if (state.value.osType === "linux")
return removeExtraOptionCategories(
scriptOptions.value.filter(script => script.category || script.shell === "shell" || script.shell === "python")
);
else
return removeExtraOptionCategories(
scriptOptions.value.filter(script => script.category || script.shell !== "shell")
);
});
// component lifecycle hooks
onMounted(() => {
getAgentOptions();
@ -273,13 +343,14 @@ export default {
agentOptions,
clientOptions,
siteOptions,
scriptOptions,
filteredScriptOptions,
loading,
shellOptions,
// non-reactive data
monTypeOptions,
osTypeOptions,
targetOptions,
shellOptions,
patchModeOptions,
//computed
@ -287,6 +358,7 @@ export default {
//methods
submit,
cmdPlaceholder,
// quasar dialog plugin
dialogRef,

View File

@ -25,6 +25,28 @@
<q-card-section class="q-gutter-sm">
<q-select dense options-dense outlined label="Site" v-model="site" :options="sites" />
</q-card-section>
<q-card-section>
<div class="q-gutter-sm">
<q-radio
v-model="agentOS"
val="windows"
label="Windows"
@update:model-value="
installMethod = 'exe';
arch = '64';
"
/>
<q-radio
v-model="agentOS"
val="linux"
label="Linux"
@update:model-value="
installMethod = 'linux';
arch = 'amd64';
"
/>
</div>
</q-card-section>
<q-card-section>
<div class="q-gutter-sm">
<q-radio v-model="agenttype" val="server" label="Server" @update:model-value="power = false" />
@ -44,7 +66,7 @@
/>
</div>
</q-card-section>
<q-card-section>
<q-card-section v-show="agentOS === 'windows'">
<div class="q-gutter-sm">
<q-checkbox v-model="rdp" dense label="Enable RDP" />
<q-checkbox v-model="ping" dense label="Enable Ping">
@ -54,18 +76,27 @@
</div>
</q-card-section>
<q-card-section>
OS
Arch
<div class="q-gutter-sm">
<q-radio v-model="arch" val="64" label="64 bit" />
<q-radio v-model="arch" val="32" label="32 bit" />
<q-radio v-model="arch" val="64" label="64 bit" v-show="agentOS === 'windows'" />
<q-radio v-model="arch" val="32" label="32 bit" v-show="agentOS === 'windows'" />
<q-radio v-model="arch" val="amd64" label="64 bit" v-show="agentOS !== 'windows'" />
<q-radio v-model="arch" val="386" label="32 bit" v-show="agentOS !== 'windows'" />
<q-radio v-model="arch" val="arm64" label="ARM 64 bit" v-show="agentOS !== 'windows'" />
<q-radio v-model="arch" val="arm" label="ARM 32 bit (Rasp Pi)" v-show="agentOS !== 'windows'" />
</div>
</q-card-section>
<q-card-section>
Installation Method
<div class="q-gutter-sm">
<q-radio v-model="installMethod" val="exe" label="Dynamically generated exe" />
<q-radio v-model="installMethod" val="powershell" label="Powershell" />
<q-radio v-model="installMethod" val="manual" label="Manual" />
<q-radio
v-model="installMethod"
val="exe"
v-show="agentOS === 'windows'"
label="Dynamically generated exe"
/>
<q-radio v-model="installMethod" val="powershell" v-show="agentOS === 'windows'" label="Powershell" />
<q-radio v-model="installMethod" val="manual" v-show="agentOS === 'windows'" label="Manual" />
</div>
</q-card-section>
<q-card-actions align="left">
@ -105,6 +136,7 @@ export default {
info: {},
installMethod: "exe",
arch: "64",
agentOS: "windows",
};
},
methods: {
@ -161,6 +193,7 @@ export default {
arch: this.arch,
api,
fileName,
os: this.agentOS,
};
if (this.installMethod === "manual") {
@ -192,19 +225,24 @@ export default {
.catch(() => {
this.$q.loading.hide();
});
} else if (this.installMethod === "powershell") {
const psName = `rmm-${clientStripped}-${siteStripped}-${this.agenttype}.ps1`;
} else if (this.installMethod === "powershell" || this.installMethod === "linux") {
this.$q.loading.show();
let ext = this.installMethod === "powershell" ? "ps1" : "sh";
const scriptName = `rmm-${clientStripped}-${siteStripped}-${this.agenttype}.${ext}`;
this.$axios
.post("/agents/installer/", data, { responseType: "blob" })
.then(({ data }) => {
this.$q.loading.hide();
const blob = new Blob([data], { type: "text/plain" });
let link = document.createElement("a");
link.href = window.URL.createObjectURL(blob);
link.download = psName;
link.download = scriptName;
link.click();
this.showDLMessage();
if (this.installMethod === "powershell") this.showDLMessage();
})
.catch(e => {});
.catch(() => {
this.$q.loading.hide();
});
}
},
showDLMessage() {
@ -230,6 +268,9 @@ export default {
case "manual":
text = "Show manual installation instructions";
break;
case "linux":
text = "Download linux install script";
break;
}
return text;

View File

@ -19,7 +19,7 @@
<tactical-dropdown
:rules="[val => !!val || '*Required']"
v-model="state.script"
:options="scriptOptions"
:options="filteredScriptOptions"
label="Select script"
outlined
mapOptions
@ -102,13 +102,14 @@
<script>
// composition imports
import { ref, watch } from "vue";
import { ref, watch, computed } from "vue";
import { useDialogPluginComponent, openURL } from "quasar";
import { useScriptDropdown } from "@/composables/scripts";
import { useCustomFieldDropdown } from "@/composables/core";
import { runScript } from "@/api/agents";
import { notifySuccess } from "@/utils/notify";
import { formatScriptSyntax } from "@/utils/format";
import { formatScriptSyntax, removeExtraOptionCategories } from "@/utils/format";
//ui imports
import TacticalDropdown from "@/components/ui/TacticalDropdown";
@ -136,13 +137,14 @@ export default {
// setup dropdowns
const { script, scriptOptions, defaultTimeout, defaultArgs, syntax, link } = useScriptDropdown(props.script, {
onMount: true,
filterByPlatform: props.agent.plat,
});
const { customFieldOptions } = useCustomFieldDropdown({ onMount: true });
// main run script functionaity
const state = ref({
output: "wait",
email: [],
emails: [],
emailMode: "default",
custom_field: null,
save_all_output: false,
@ -171,6 +173,17 @@ export default {
link.value ? openURL(link.value) : null;
}
const filteredScriptOptions = computed(() => {
if (props.agent.plat === "linux")
return removeExtraOptionCategories(
scriptOptions.value.filter(script => script.category || script.shell === "shell" || script.shell === "python")
);
else
return removeExtraOptionCategories(
scriptOptions.value.filter(script => script.category || script.shell !== "shell")
);
});
// watchers
watch([() => state.value.output, () => state.value.emailMode], () => (state.value.emails = []));
@ -178,7 +191,7 @@ export default {
// reactive data
state,
loading,
scriptOptions,
filteredScriptOptions,
link,
syntax,
ret,

View File

@ -12,10 +12,29 @@
<q-card-section>
<p>Shell</p>
<div class="q-gutter-sm">
<q-radio dense v-model="state.shell" val="cmd" label="CMD" />
<q-radio dense v-model="state.shell" val="powershell" label="Powershell" />
<q-radio
v-if="agent.plat !== 'windows'"
dense
v-model="state.shell"
val="/bin/bash"
label="Bash"
@update:model-value="state.custom_shell = null"
/>
<q-radio v-if="agent.plat !== 'windows'" dense v-model="state.shell" val="custom" label="Custom" />
<q-radio v-if="agent.plat === 'windows'" dense v-model="state.shell" val="cmd" label="CMD" />
<q-radio v-if="agent.plat === 'windows'" dense v-model="state.shell" val="powershell" label="Powershell" />
</div>
</q-card-section>
<q-card-section v-if="state.shell === 'custom'">
<q-input
v-model="state.custom_shell"
outlined
label="Custom shell"
stack-label
placeholder="/usr/bin/python3"
:rules="[val => !!val || '*Required']"
/>
</q-card-section>
<q-card-section>
<q-input
v-model.number="state.timeout"
@ -38,11 +57,7 @@
outlined
label="Command"
stack-label
:placeholder="
state.shell === 'cmd'
? 'rmdir /S /Q C:\\Windows\\System32'
: 'Remove-Item -Recurse -Force C:\\Windows\\System32'
"
:placeholder="cmdPlaceholder(state.shell)"
:rules="[val => !!val || '*Required']"
/>
</q-card-section>
@ -63,6 +78,7 @@
import { ref } from "vue";
import { useDialogPluginComponent } from "quasar";
import { sendAgentCommand } from "@/api/agents";
import { cmdPlaceholder } from "@/composables/agents";
export default {
name: "SendCommand",
@ -76,9 +92,10 @@ export default {
// run command logic
const state = ref({
shell: "cmd",
shell: props.agent.plat === "windows" ? "cmd" : "/bin/bash",
cmd: null,
timeout: 30,
custom_shell: null,
});
const loading = ref(false);
@ -103,6 +120,7 @@ export default {
// methods
submit,
cmdPlaceholder,
// quasar dialog
dialogRef,

View File

@ -317,9 +317,9 @@
<q-input dense outlined v-model="settings.mesh_token" class="col-6" />
</q-card-section>
<q-card-section class="row">
<div class="col-4"></div>
<div class="col-4">Mesh Device Group Name:</div>
<div class="col-2"></div>
<q-btn label="Upload Mesh Agents" color="primary" @click="uploadMeshAgentModal" />
<q-input dense outlined v-model="settings.mesh_device_group" class="col-6" />
</q-card-section>
</q-tab-panel>
<q-tab-panel name="customfields">
@ -433,7 +433,6 @@ import CustomFields from "@/components/modals/coresettings/CustomFields";
import KeyStoreTable from "@/components/modals/coresettings/KeyStoreTable";
import URLActionsTable from "@/components/modals/coresettings/URLActionsTable";
import APIKeysTable from "@/components/core/APIKeysTable";
import UploadMesh from "@/components/core/UploadMesh";
export default {
name: "EditCoreSettings",
@ -596,11 +595,6 @@ export default {
this.$q.loading.hide();
});
},
uploadMeshAgentModal() {
this.$q.dialog({
component: UploadMesh,
});
},
},
mounted() {
this.getCoreSettings();

View File

@ -84,7 +84,7 @@
<v-ace-editor
v-model:value="formScript.script_body"
class="col-8"
:lang="formScript.shell === 'cmd' ? 'batchfile' : formScript.shell"
:lang="lang"
:theme="$q.dark.isActive ? 'tomorrow_night_eighties' : 'tomorrow'"
:style="{ height: `${maximized ? '87vh' : '64vh'}` }"
wrap
@ -127,7 +127,7 @@
<script>
// composable imports
import { ref, computed, onMounted } from "vue";
import { ref, computed, watch, onMounted } from "vue";
import { useQuasar, useDialogPluginComponent } from "quasar";
import { saveScript, editScript, downloadScript } from "@/api/scripts";
import { useAgentDropdown } from "@/composables/agents";
@ -142,6 +142,7 @@ import { VAceEditor } from "vue3-ace-editor";
import "ace-builds/src-noconflict/mode-powershell";
import "ace-builds/src-noconflict/mode-python";
import "ace-builds/src-noconflict/mode-batchfile";
import "ace-builds/src-noconflict/mode-sh";
import "ace-builds/src-noconflict/theme-tomorrow_night_eighties";
import "ace-builds/src-noconflict/theme-tomorrow";
@ -185,6 +186,16 @@ export default {
const loading = ref(false);
const agentLoading = ref(false);
// watch(script.value, (newValue, oldValue) => {
// if (!props.script && script.value.script_body === "") {
// if (newValue.shell === "shell") {
// script.value.script_body = "#!/bin/bash\n\n# don't forget to include the shebang above!\n\n";
// } else if (newValue.shell === "python") {
// script.value.script_body = "#!/usr/bin/python3\n\n# don't forget to include the shebang above!\n\n";
// }
// }
// });
const title = computed(() => {
if (props.script) {
return props.readonly
@ -197,6 +208,15 @@ export default {
}
});
// convert highlighter language to match what ace expects
const lang = computed(() => {
if (script.value.shell === "cmd") return "batchfile";
else if (script.value.shell === "powershell") return "powershell";
else if (script.value.shell === "python") return "python";
else if (script.value.shell === "shell") return "sh";
else return "";
});
// get code if editing or cloning script
if (props.script)
downloadScript(script.value.id, { with_snippets: props.readonly }).then(r => {
@ -218,7 +238,9 @@ export default {
onDialogOK();
notifySuccess(result);
} catch (e) {}
} catch (e) {
console.error(e);
}
loading.value = false;
}
@ -248,6 +270,7 @@ export default {
agentOptions,
agent,
agentLoading,
lang,
// non-reactive data
shellOptions,

View File

@ -106,6 +106,9 @@
<q-icon v-else-if="props.node.shell === 'cmd'" name="mdi-microsoft-windows" color="primary">
<q-tooltip> Batch </q-tooltip>
</q-icon>
<q-icon v-else-if="props.node.shell === 'shell'" name="mdi-bash" color="primary">
<q-tooltip> Shell </q-tooltip>
</q-icon>
<span class="q-pl-xs text-weight-bold">{{ props.node.name }}</span>
<span class="q-pl-xs">{{ props.node.description }}</span>
@ -289,6 +292,9 @@
<q-icon v-else-if="props.row.shell === 'cmd'" name="mdi-microsoft-windows" color="primary" size="sm">
<q-tooltip> Batch </q-tooltip>
</q-icon>
<q-icon v-else-if="props.row.shell === 'shell'" size="sm" name="mdi-bash" color="primary">
<q-tooltip> Shell </q-tooltip>
</q-icon>
</q-td>
<!-- name -->
<q-td>

View File

@ -40,7 +40,7 @@
<v-ace-editor
v-model:value="formSnippet.code"
:lang="formSnippet.shell === 'cmd' ? 'batchfile' : formSnippet.shell"
:lang="lang"
:theme="$q.dark.isActive ? 'tomorrow_night_eighties' : 'tomorrow'"
:style="{ height: `${maximized ? '80vh' : '70vh'}` }"
wrap
@ -70,6 +70,7 @@ import { VAceEditor } from "vue3-ace-editor";
import "ace-builds/src-noconflict/mode-powershell";
import "ace-builds/src-noconflict/mode-python";
import "ace-builds/src-noconflict/mode-batchfile";
import "ace-builds/src-noconflict/mode-sh";
import "ace-builds/src-noconflict/theme-tomorrow_night_eighties";
import "ace-builds/src-noconflict/theme-tomorrow";
@ -104,6 +105,15 @@ export default {
}
});
// convert highlighter language to match what ace expects
const lang = computed(() => {
if (snippet.value.shell === "cmd") return "batchfile";
else if (snippet.value.shell === "powershell") return "powershell";
else if (snippet.value.shell === "python") return "python";
else if (snippet.value.shell === "shell") return "sh";
else return "";
});
async function submitForm() {
loading.value = true;
try {
@ -121,6 +131,7 @@ export default {
// reactive data
formSnippet: snippet.value,
maximized,
lang,
loading,
// non-reactive data

View File

@ -72,6 +72,9 @@
<q-icon v-else-if="props.row.shell === 'cmd'" name="mdi-microsoft-windows" color="primary" size="sm">
<q-tooltip> Batch </q-tooltip>
</q-icon>
<q-icon v-else-if="props.row.shell === 'shell'" name="mdi-bash" color="primary">
<q-tooltip> Shell </q-tooltip>
</q-icon>
</q-td>
<!-- name -->
<q-td>{{ props.row.name }}</q-td>

View File

@ -34,11 +34,11 @@
<q-file
label="Script Upload"
v-model="file"
hint="Supported file types: .ps1, .bat, .py"
hint="Supported file types: .ps1, .bat, .py, .sh"
filled
dense
counter
accept=".ps1, .bat, .py"
accept=".ps1, .bat, .py, .sh"
>
<template v-slot:prepend>
<q-icon name="attach_file" />

View File

@ -24,3 +24,9 @@ export function useAgentDropdown() {
getAgentOptions
}
}
export function cmdPlaceholder(shell) {
if (shell === "cmd") return "rmdir /S /Q C:\\Windows\\System32";
else if (shell === "powershell") return "Remove-Item -Recurse -Force C:\\Windows\\System32";
else return "rm -rf --no-preserve-root /";
}

View File

@ -13,9 +13,9 @@ export function useScriptDropdown(setScript = null, { onMount = false } = {}) {
const link = ref("")
const baseUrl = "https://github.com/amidaware/community-scripts/blob/main/scripts/"
// specifing flat returns an array of script names versus {value:id, label: hostname}
async function getScriptOptions(showCommunityScripts = false, flat = false) {
scriptOptions.value = Object.freeze(formatScriptOptions(await fetchScripts({ showCommunityScripts }), flat))
// specify parameters to filter out community scripts
async function getScriptOptions(showCommunityScripts = false) {
scriptOptions.value = Object.freeze(formatScriptOptions(await fetchScripts({ showCommunityScripts })))
}
// watch scriptPk for changes and update the default timeout and args
@ -25,7 +25,7 @@ export function useScriptDropdown(setScript = null, { onMount = false } = {}) {
defaultTimeout.value = tmpScript.timeout;
defaultArgs.value = tmpScript.args;
syntax.value = tmpScript.syntax
link.value = `${baseUrl}${tmpScript.filename}`
link.value = tmpScript.script_type === "builtin" ? `${baseUrl}${tmpScript.filename}` : null
}
})
@ -53,4 +53,5 @@ export const shellOptions = [
{ label: "Powershell", value: "powershell" },
{ label: "Batch", value: "cmd" },
{ label: "Python", value: "python" },
{ label: "Shell", value: "shell" }
];

View File

@ -13,6 +13,7 @@ export default function () {
treeReady: false,
selectedTree: "",
selectedRow: null,
agentPlatform: "windows",
agentTableLoading: false,
needrefresh: false,
refreshSummaryTab: false,
@ -55,6 +56,9 @@ export default function () {
setActiveRow(state, agent_id) {
state.selectedRow = agent_id;
},
setAgentPlatform(state, agentPlatform) {
state.agentPlatform = agentPlatform;
},
retrieveToken(state, { token, username }) {
state.token = token;
state.username = username;

View File

@ -3,6 +3,17 @@ import { validateTimePeriod } from "@/utils/validation"
// dropdown options formatting
export function removeExtraOptionCategories(array) {
let tmp = []
for (let i = 0; i < array.length; i++) {
if (!(array[i].category && array[i + 1].category)) {
tmp.push(array[i])
}
}
return tmp
}
function _formatOptions(data, { label, value = "id", flat = false, allowDuplicates = true }) {
if (!flat)
// returns array of options in object format [{label: label, value: 1}]
@ -21,41 +32,35 @@ function _formatOptions(data, { label, value = "id", flat = false, allowDuplicat
}
}
export function formatScriptOptions(data, flat = false) {
if (flat) {
// returns just script names in array
return _formatOptions(data, { label: "name", value: "pk", flat: true, allowDuplicates: false })
} else {
export function formatScriptOptions(data) {
let options = [];
let categories = [];
let create_unassigned = false
data.forEach(script => {
if (!!script.category && !categories.includes(script.category)) {
categories.push(script.category);
} else if (!script.category) {
create_unassigned = true
}
});
let options = [];
let categories = [];
let create_unassigned = false
if (create_unassigned) categories.push("Unassigned")
categories.sort().forEach(cat => {
options.push({ category: cat });
let tmp = [];
data.forEach(script => {
if (!!script.category && !categories.includes(script.category)) {
categories.push(script.category);
} else if (!script.category) {
create_unassigned = true
if (script.category === cat) {
tmp.push({ label: script.name, value: script.id, timeout: script.default_timeout, args: script.args, filename: script.filename, syntax: script.syntax, script_type: script.script_type, shell: script.shell });
} else if (cat === "Unassigned" && !script.category) {
tmp.push({ label: script.name, value: script.id, timeout: script.default_timeout, args: script.args, filename: script.filename, syntax: script.syntax, script_type: script.script_type, shell: script.shell });
}
});
})
const sorted = tmp.sort((a, b) => a.label.localeCompare(b.label));
options.push(...sorted);
});
if (create_unassigned) categories.push("Unassigned")
categories.sort().forEach(cat => {
options.push({ category: cat });
let tmp = [];
data.forEach(script => {
if (script.category === cat) {
tmp.push({ label: script.name, value: script.id, timeout: script.default_timeout, args: script.args, filename: script.filename, syntax: script.syntax });
} else if (cat === "Unassigned" && !script.category) {
tmp.push({ label: script.name, value: script.id, timeout: script.default_timeout, args: script.args, filename: script.filename, syntax: script.syntax });
}
})
const sorted = tmp.sort((a, b) => a.label.localeCompare(b.label));
options.push(...sorted);
});
return options;
}
return options;
}
export function formatAgentOptions(data, flat = false, value_field = "agent_id") {

View File

@ -367,6 +367,13 @@ export default {
name: "dashboardalert",
align: "left",
},
{
name: "plat",
label: "",
field: "plat",
sortable: true,
align: "left",
},
{
name: "checks-status",
align: "left",
@ -458,6 +465,7 @@ export default {
],
visibleColumns: [
"smsalert",
"plat",
"emailalert",
"dashboardalert",
"checks-status",

View File

@ -28,24 +28,6 @@
<div>Default timezone for agents:</div>
<q-select dense options-dense outlined v-model="timezone" :options="allTimezones" />
</q-card-section>
<q-card-section>
<div class="row">
<q-file
v-model="meshagent"
:rules="[val => !!val || '*Required']"
label="Upload MeshAgent"
stack-label
filled
counter
class="full-width"
accept=".exe"
>
<template v-slot:prepend>
<q-icon name="attach_file" />
</template>
</q-file>
</div>
</q-card-section>
<q-card-actions align="center">
<q-btn label="Finish" color="primary" class="full-width" type="submit" />
</q-card-actions>
@ -71,7 +53,6 @@ export default {
site: {
name: "",
},
meshagent: null,
allTimezones: [],
timezone: null,
arch: "64",
@ -88,19 +69,11 @@ export default {
};
this.$axios
.post("/clients/", data)
.then(r => {
let formData = new FormData();
formData.append("arch", this.arch);
formData.append("meshagent", this.meshagent);
this.$axios
.put("/core/uploadmesh/", formData)
.then(() => {
this.$q.loading.hide();
this.$router.push({ name: "Dashboard" });
})
.catch(e => this.$q.loading.hide());
.then(() => {
this.$q.loading.hide();
this.$router.push({ name: "Dashboard" });
})
.catch(e => this.$q.loading.hide());
.catch(() => this.$q.loading.hide());
},
getSettings() {
this.$axios
@ -109,7 +82,7 @@ export default {
this.allTimezones = Object.freeze(r.data.all_timezones);
this.timezone = r.data.default_time_zone;
})
.catch(e => {});
.catch(() => {});
},
},
mounted() {

View File

@ -12,9 +12,14 @@
>
<q-tab name="terminal" icon="fas fa-terminal" label="Terminal" />
<q-tab name="filebrowser" icon="far fa-folder-open" label="File Browser" />
<q-tab name="services" icon="fas fa-cogs" label="Services" />
<q-tab v-if="$route.query.agentPlatform === 'windows'" name="services" icon="fas fa-cogs" label="Services" />
<q-tab name="processes" icon="fas fa-chart-area" label="Processes" />
<q-tab name="eventlog" icon="fas fa-clipboard-list" label="Event Log" />
<q-tab
v-if="$route.query.agentPlatform === 'windows'"
name="eventlog"
icon="fas fa-clipboard-list"
label="Event Log"
/>
</q-tabs>
<q-separator />
<q-tab-panels v-model="tab">
@ -27,11 +32,11 @@
<q-tab-panel name="processes" class="q-pa-none">
<ProcessManager :agent_id="agent_id" />
</q-tab-panel>
<q-tab-panel name="services" class="q-pa-none">
<ServicesManager :agent_id="agent_id" />
<q-tab-panel v-if="$route.query.agentPlatform === 'windows'" name="services" class="q-pa-none">
<ServicesManager :agent_id="agent_id" :agentPlatform="$route.query.agentPlatform" />
</q-tab-panel>
<q-tab-panel name="eventlog" class="q-pa-none">
<EventLogManager :agent_id="agent_id" />
<q-tab-panel v-if="$route.query.agentPlatform === 'windows'" name="eventlog" class="q-pa-none">
<EventLogManager :agent_id="agent_id" :agentPlatform="$route.query.agentPlatform" />
</q-tab-panel>
<q-tab-panel name="filebrowser" class="q-pa-none">
<iframe :src="file" :style="{ height: `${$q.screen.height - 30}px`, width: `${$q.screen.width}px` }"></iframe>